diff --git a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png index d0dc89b9ff..b51f1d9428 100644 Binary files a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png and b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png index 54b04299e5..997cc0bba9 100644 Binary files a/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png and b/Images.xcassets/Chat/Input/Media/SettingsIcon.imageset/StickerKeyboardSettingsIcon@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index d61153abc7..d49e1a8a6f 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -80,7 +80,6 @@ D02D60C8206E705D00FEFE1E /* SecureIdValueFormPhoneItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60C7206E705D00FEFE1E /* SecureIdValueFormPhoneItem.swift */; }; D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */; }; D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */; }; - D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DA8204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift */; }; D0380DAB204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */; }; D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */; }; @@ -234,7 +233,6 @@ D09D88731F86D56B00BEB4C9 /* AuthorizationLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D88721F86D56B00BEB4C9 /* AuthorizationLayout.swift */; }; D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */; }; D09E637F1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */; }; - D09E63A21F0FA723003444CD /* EmbedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A11F0FA723003444CD /* EmbedVideoNode.swift */; }; D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */; }; D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63AF1F1010FE003444CD /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63B11F11289A003444CD /* PassKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -271,6 +269,8 @@ D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; }; D0B4AF881EC112EE00D51FF6 /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B4AF871EC112ED00D51FF6 /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */; }; + D0B69C3920EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B69C3820EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift */; }; + D0B69C3C20EBD8C8003632C7 /* CheckDeviceAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B69C3B20EBD8C8003632C7 /* CheckDeviceAccess.swift */; }; D0B85C1C1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C1B1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift */; }; D0B85C1E1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C1D1FF6F76600E795B4 /* AuthorizationSequencePasswordRecoveryControllerNode.swift */; }; D0B85C211FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B85C201FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift */; }; @@ -318,6 +318,7 @@ D0CFBB951FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB941FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift */; }; D0CFBB971FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB961FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift */; }; D0D4345C1F97CEAA00CC1806 /* ProxyServerSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4345B1F97CEAA00CC1806 /* ProxyServerSettingsController.swift */; }; + D0D9DE0D20EFEA2E00F20B06 /* InstantPageMediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D9DE0C20EFEA2E00F20B06 /* InstantPageMediaPlaylist.swift */; }; D0DE5803205AEB7600C356A8 /* include in Resources */ = {isa = PBXBuildFile; fileRef = D0DE5802205AEB7600C356A8 /* include */; }; D0DE5805205B202500C356A8 /* ScreenCaptureDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE5804205B202500C356A8 /* ScreenCaptureDetection.swift */; }; D0DE66061F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */; }; @@ -469,6 +470,7 @@ D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */ = {isa = PBXBuildFile; fileRef = D0EB41FF1F30ED4F00838FE6 /* LegacyImageProcessors.m */; }; D0EB42051F3143AB00838FE6 /* LegacyComponentsResources.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D0EB42041F3143AB00838FE6 /* LegacyComponentsResources.bundle */; }; D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */; }; + D0EC55A3210231D600D1992C /* SearchPeerMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC55A2210231D600D1992C /* SearchPeerMembers.swift */; }; D0EC6CAE1EB9F58800EBF1C3 /* animations.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2CC1E48797500650E93 /* animations.c */; }; D0EC6CAF1EB9F58800EBF1C3 /* buffer.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2CE1E48797500650E93 /* buffer.c */; }; D0EC6CB01EB9F58800EBF1C3 /* objects.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2D41E48797500650E93 /* objects.c */; }; @@ -540,13 +542,10 @@ D0EC6CF51EB9F58800EBF1C3 /* PeerMessageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */; }; D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */; }; D0EC6CF71EB9F58800EBF1C3 /* RecentGifManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */; }; - D0EC6CF81EB9F58800EBF1C3 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; }; D0EC6CF91EB9F58800EBF1C3 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */; }; D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */; }; D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; }; - D0EC6CFC1EB9F58800EBF1C3 /* ManagedAudioPlaylistPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */; }; D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */; }; - D0EC6CFE1EB9F58800EBF1C3 /* PeerMediaAudioPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */; }; D0EC6CFF1EB9F58800EBF1C3 /* OverlayMediaController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B421EB92DF600EBF1C3 /* OverlayMediaController.swift */; }; D0EC6D001EB9F58800EBF1C3 /* OverlayMediaControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B441EB92E5A00EBF1C3 /* OverlayMediaControllerNode.swift */; }; D0EC6D021EB9F58800EBF1C3 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; }; @@ -613,7 +612,6 @@ D0EC6D3F1EB9F58800EBF1C3 /* MediaNavigationAccessoryPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */; }; D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */; }; D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */; }; - D0EC6D421EB9F58800EBF1C3 /* MediaNavigationAccessoryItemListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */; }; D0EC6D4B1EB9F58800EBF1C3 /* ChatListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF781DCA226F00761F81 /* ChatListNode.swift */; }; D0EC6D4D1EB9F58800EBF1C3 /* ChatListHoleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */; }; D0EC6D4E1EB9F58800EBF1C3 /* ChatListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */; }; @@ -964,12 +962,10 @@ D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */; }; D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */; }; - D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */; }; D0FE4DE41F0AEBB900E8A0B3 /* SharedVideoContextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */; }; D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */; }; D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */; }; D0FFF7F81F55B83600BEBC01 /* InstantPageAudioNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */; }; - D0FFF7FF1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -1042,7 +1038,6 @@ D01776BB1F1E21AF0044446D /* RadialStatusBackgroundNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialStatusBackgroundNode.swift; sourceTree = ""; }; D01776BD1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionSectionsNode.swift; sourceTree = ""; }; D0177B7F1DFAE18500A5083A /* MediaPlayerTimeTextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerTimeTextNode.swift; sourceTree = ""; }; - D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryItemListNode.swift; sourceTree = ""; }; D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMediaResourceStatus.swift; sourceTree = ""; }; D018477D1FFBC01E00075256 /* TimestampStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampStrings.swift; sourceTree = ""; }; D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListPresentationData.swift; sourceTree = ""; }; @@ -1148,7 +1143,6 @@ D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveMediaBadge.swift; sourceTree = ""; }; D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemVideoContent.swift; sourceTree = ""; }; D03120F51DA534C1006A2A60 /* ItemListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActionItem.swift; sourceTree = ""; }; - D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramVideoNode.swift; sourceTree = ""; }; D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAdminsController.swift; sourceTree = ""; }; D0380DA8204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretMediaPreviewFooterContentNode.swift; sourceTree = ""; }; D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialStatusSecretTimeoutContentNode.swift; sourceTree = ""; }; @@ -1382,8 +1376,6 @@ D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = ""; }; D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeEssentialGraphics.swift; sourceTree = ""; }; D06FFBA91EAFAD2500CB53D4 /* PresentationResourcesChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChat.swift; sourceTree = ""; }; - D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioPlaylistPlayer.swift; sourceTree = ""; }; - D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaAudioPlaylist.swift; sourceTree = ""; }; D0736F241DF4D0E500F2C02A /* TelegramController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramController.swift; sourceTree = ""; }; D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryPanel.swift; sourceTree = ""; }; D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryContainerNode.swift; sourceTree = ""; }; @@ -1490,7 +1482,6 @@ D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedMediaId.swift; sourceTree = ""; }; - D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedVideoNode.swift; sourceTree = ""; }; D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = ""; }; D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = ""; }; D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextEmptyStateItem.swift; sourceTree = ""; }; @@ -1499,7 +1490,6 @@ D09D88721F86D56B00BEB4C9 /* AuthorizationLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationLayout.swift; sourceTree = ""; }; D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedMediaPlayer.swift; sourceTree = ""; }; D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessagesMediaPlaylist.swift; sourceTree = ""; }; - D09E63A11F0FA723003444CD /* EmbedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbedVideoNode.swift; sourceTree = ""; }; D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PictureInPictureVideoControlsNode.swift; sourceTree = ""; }; D09E63AF1F1010FE003444CD /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; D09E63B11F11289A003444CD /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; @@ -1546,6 +1536,8 @@ D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = ""; }; D0B4AF871EC112ED00D51FF6 /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; }; D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitIntergation.swift; sourceTree = ""; }; + D0B69C3820EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveInstantVideoNode.swift; sourceTree = ""; }; + D0B69C3B20EBD8C8003632C7 /* CheckDeviceAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckDeviceAccess.swift; sourceTree = ""; }; D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionController.swift; sourceTree = ""; }; D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionControllerNode.swift; sourceTree = ""; }; D0B843911DA7F13E005F29E1 /* ItemListDisclosureItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListDisclosureItem.swift; sourceTree = ""; }; @@ -1662,6 +1654,7 @@ D0D748051E7AF63800F4B1F6 /* StickerPackPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewController.swift; sourceTree = ""; }; D0D748071E7AF64400F4B1F6 /* StickerPackPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewControllerNode.swift; sourceTree = ""; }; D0D7480E1E7B1BD600F4B1F6 /* StickerPackPreviewGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewGridItem.swift; sourceTree = ""; }; + D0D9DE0C20EFEA2E00F20B06 /* InstantPageMediaPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageMediaPlaylist.swift; sourceTree = ""; }; D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressNavigationButtonNode.swift; sourceTree = ""; }; D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShakeAnimation.swift; sourceTree = ""; }; D0DC35431DE32230000195EB /* ChatInterfaceStateContextQueries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextQueries.swift; sourceTree = ""; }; @@ -1848,6 +1841,7 @@ D0EB41FF1F30ED4F00838FE6 /* LegacyImageProcessors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LegacyImageProcessors.m; sourceTree = ""; }; D0EB42041F3143AB00838FE6 /* LegacyComponentsResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = LegacyComponentsResources.bundle; path = ../LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle; sourceTree = ""; }; D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionEmptyNode.swift; sourceTree = ""; }; + D0EC55A2210231D600D1992C /* SearchPeerMembers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPeerMembers.swift; sourceTree = ""; }; D0EC6B351EB88D0A00EBF1C3 /* ThemeGridController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridController.swift; sourceTree = ""; }; D0EC6B371EB88D1600EBF1C3 /* ThemeGridControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerNode.swift; sourceTree = ""; }; D0EC6B3A1EB8CF2B00EBF1C3 /* CallController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; @@ -2013,12 +2007,10 @@ D0FC408F1D5B8E7500261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePeerNode.swift; sourceTree = ""; }; D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationSurfaceLevels.swift; sourceTree = ""; }; - D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantVideoNode.swift; sourceTree = ""; }; D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedVideoContextManager.swift; sourceTree = ""; }; D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaItemNode.swift; sourceTree = ""; }; D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioItem.swift; sourceTree = ""; }; D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioNode.swift; sourceTree = ""; }; - D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageMediaAudioPlaylist.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2625,7 +2617,6 @@ D0736F291DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift */, D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */, D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */, - D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */, D09394122007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift */, D04B26EB20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift */, D0BE303120601FFC00FBE6D8 /* LocationBroadcastActionSheetItem.swift */, @@ -2936,8 +2927,7 @@ children = ( D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */, D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */, - D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */, - D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */, + D0D9DE0C20EFEA2E00F20B06 /* InstantPageMediaPlaylist.swift */, ); name = "Shared Media Player"; sourceTree = ""; @@ -2962,6 +2952,14 @@ name = Calls; sourceTree = ""; }; + D0B69C3A20EBD8B3003632C7 /* Device Access */ = { + isa = PBXGroup; + children = ( + D0B69C3B20EBD8C8003632C7 /* CheckDeviceAccess.swift */, + ); + name = "Device Access"; + sourceTree = ""; + }; D0B7F8DF1D8A17D20045D939 /* Collection */ = { isa = PBXGroup; children = ( @@ -3591,20 +3589,15 @@ D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */, D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */, D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */, - D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */, D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */, D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */, D0CFBB901FD881A600B65C0D /* AudioRecordningToneData.swift */, - D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */, D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */, D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */, D0EC6B421EB92DF600EBF1C3 /* OverlayMediaController.swift */, D0EC6B441EB92E5A00EBF1C3 /* OverlayMediaControllerNode.swift */, D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */, D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */, - D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */, - D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */, - D09E63A11F0FA723003444CD /* EmbedVideoNode.swift */, D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */, D09E637D1F0E8C66003444CD /* Shared Media Player */, D0D03AE61DECB0D200220C46 /* Audio Recorder */, @@ -3883,6 +3876,7 @@ D0E8174B2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift */, D0E8174D2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift */, D0E8174F2012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift */, + D0B69C3820EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift */, D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */, ); name = Items; @@ -4051,6 +4045,7 @@ D025A4241F79428300563950 /* Fetch Manager */, D046142C2004DB1D00EC0EF2 /* Live Location Manager */, D0383ED5207D19BC00C45548 /* Emoji */, + D0B69C3A20EBD8B3003632C7 /* Device Access */, D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, @@ -4099,6 +4094,7 @@ D0CAD8FA20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift */, D0CAD8FC20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift */, D044A0F220BDA05800326FAC /* ThrottledValue.swift */, + D0EC55A2210231D600D1992C /* SearchPeerMembers.swift */, ); name = Utils; sourceTree = ""; @@ -4572,7 +4568,6 @@ D056CD7A1FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift in Sources */, D0BE30472061C0BC00FBE6D8 /* SecureIdAuthPasswordOptionContentNode.swift in Sources */, D0EC6CF71EB9F58800EBF1C3 /* RecentGifManagedMediaId.swift in Sources */, - D0EC6CF81EB9F58800EBF1C3 /* ManagedVideoNode.swift in Sources */, D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */, D0E9BA611F055A4300F079A4 /* STPDelegateProxy.m in Sources */, D0EC6CF91EB9F58800EBF1C3 /* MediaManager.swift in Sources */, @@ -4582,9 +4577,7 @@ D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */, D048B339203C532800038D05 /* ChatMediaInputPane.swift in Sources */, D0E817502012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift in Sources */, - D0EC6CFC1EB9F58800EBF1C3 /* ManagedAudioPlaylistPlayer.swift in Sources */, D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */, - D0EC6CFE1EB9F58800EBF1C3 /* PeerMediaAudioPlaylist.swift in Sources */, D0EC6CFF1EB9F58800EBF1C3 /* OverlayMediaController.swift in Sources */, D0EC6D001EB9F58800EBF1C3 /* OverlayMediaControllerNode.swift in Sources */, D0EC6D021EB9F58800EBF1C3 /* diag_range.c in Sources */, @@ -4615,6 +4608,7 @@ D0EC6D121EB9F58800EBF1C3 /* VideoPlayerProxy.swift in Sources */, D0EC6D131EB9F58800EBF1C3 /* MediaTrackDecodableFrame.swift in Sources */, D0EC6D141EB9F58800EBF1C3 /* MediaTrackFrame.swift in Sources */, + D0B69C3920EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift in Sources */, D0EC6D151EB9F58800EBF1C3 /* MediaTrackFrameBuffer.swift in Sources */, D0EC6D161EB9F58800EBF1C3 /* MediaTrackFrameDecoder.swift in Sources */, D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */, @@ -4629,6 +4623,7 @@ D0EC6D1B1EB9F58800EBF1C3 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, D01C06AF1FBB461E001561AB /* JoinLinkPreviewController.swift in Sources */, D0EC6D1C1EB9F58800EBF1C3 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, + D0D9DE0D20EFEA2E00F20B06 /* InstantPageMediaPlaylist.swift in Sources */, D0EC6D1D1EB9F58800EBF1C3 /* FFMpegPacket.swift in Sources */, D01C06B11FBB4643001561AB /* JoinLinkPreviewControllerNode.swift in Sources */, D0EC6D1E1EB9F58800EBF1C3 /* MediaPlayerScrubbingNode.swift in Sources */, @@ -4670,6 +4665,7 @@ D0EC6D2C1EB9F58800EBF1C3 /* TouchDownGestureRecognizer.swift in Sources */, D0EC6D2D1EB9F58800EBF1C3 /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */, D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */, + D0B69C3C20EBD8C8003632C7 /* CheckDeviceAccess.swift in Sources */, D0FA08C020483F9600DD23FC /* ExtractVideoData.swift in Sources */, D0BE30492061C0F500FBE6D8 /* SecureIdAuthHeaderNode.swift in Sources */, D0EC6D2E1EB9F58800EBF1C3 /* ImageNode.swift in Sources */, @@ -4699,7 +4695,6 @@ D0147BAB206EA6C100E40378 /* SecureIdDocumentImageGalleryItem.swift in Sources */, D0AD02EC20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift in Sources */, D0EC6D3D1EB9F58800EBF1C3 /* ProgressNavigationButtonNode.swift in Sources */, - D0FFF7FF1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift in Sources */, D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */, D0EC6D3E1EB9F58800EBF1C3 /* TelegramController.swift in Sources */, D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */, @@ -4712,7 +4707,6 @@ D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */, D097C26820DD0A1D007BB4B8 /* PeerReportController.swift in Sources */, D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */, - D0EC6D421EB9F58800EBF1C3 /* MediaNavigationAccessoryItemListNode.swift in Sources */, D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */, D0EC6D4B1EB9F58800EBF1C3 /* ChatListNode.swift in Sources */, D0EC6D4D1EB9F58800EBF1C3 /* ChatListHoleItem.swift in Sources */, @@ -4874,7 +4868,6 @@ D0EC6D9F1EB9F58900EBF1C3 /* ChatUnreadItem.swift in Sources */, D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */, D0EC6DA01EB9F58900EBF1C3 /* ChatHoleItem.swift in Sources */, - D09E63A21F0FA723003444CD /* EmbedVideoNode.swift in Sources */, D093D82020699A7300BC3599 /* FormController.swift in Sources */, D0EC6DA11EB9F58900EBF1C3 /* ChatMessageSelectionNode.swift in Sources */, D0EC6DA21EB9F58900EBF1C3 /* ChatMessageBubbleImages.swift in Sources */, @@ -5018,7 +5011,6 @@ D0EC6DF61EB9F58900EBF1C3 /* PeerMediaCollectionControllerNode.swift in Sources */, D0477D1D1F617E8900412B44 /* NativeVideoContent.swift in Sources */, D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */, - D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */, D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */, D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */, @@ -5068,7 +5060,6 @@ D0AD02EA1FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift in Sources */, D0EC6E141EB9F58900EBF1C3 /* InstantPageMedia.swift in Sources */, D0EC6E151EB9F58900EBF1C3 /* InstantPageLinkSelectionView.swift in Sources */, - D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */, D0EC6E161EB9F58900EBF1C3 /* InstantPageLayoutSpacings.swift in Sources */, D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */, D0EC6E181EB9F58900EBF1C3 /* InstantPageTextItem.swift in Sources */, @@ -5113,6 +5104,7 @@ D093D81D206994FD00BC3599 /* FindSecureIdValue.swift in Sources */, D0EC6E321EB9F58900EBF1C3 /* CreateGroupController.swift in Sources */, D00BED221F73F82400922292 /* SharePeersContainerNode.swift in Sources */, + D0EC55A3210231D600D1992C /* SearchPeerMembers.swift in Sources */, D0EC6E331EB9F58900EBF1C3 /* CreateChannelController.swift in Sources */, D0AA29AE1F72770D00C050AC /* ChatListItemStrings.swift in Sources */, D0EC6E341EB9F58900EBF1C3 /* ItemListItem.swift in Sources */, diff --git a/TelegramUI/AuthorizationSequenceCodeEntryController.swift b/TelegramUI/AuthorizationSequenceCodeEntryController.swift index 8e86e276a3..da528b781a 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryController.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryController.swift @@ -96,7 +96,24 @@ final class AuthorizationSequenceCodeEntryController: ViewController { } @objc func nextPressed() { - if self.controllerNode.currentCode.isEmpty { + guard let (_, type, _, _) = self.data else { + return + } + + var minimalCodeLength = 1 + + switch type { + case let .otherSession(length): + minimalCodeLength = Int(length) + case let .sms(length): + minimalCodeLength = Int(length) + case let .call(length): + minimalCodeLength = Int(length) + case .flashCall: + break + } + + if self.controllerNode.currentCode.count < minimalCodeLength { hapticFeedback.error() self.controllerNode.animateError() } else { diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index 137f5b4fc2..da689ef73d 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -241,6 +241,9 @@ public func shouldDownloadMediaAutomatically(settings: AutomaticMediaDownloadSet guard let peer = peer else { return false } + if let file = media as? TelegramMediaFile, file.isSticker { + return true + } if let (category, size) = categoryForPeerAndMedia(settings: settings, peer: peer, media: media) { if let size = size { return category.cellular && size <= category.sizeLimit diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 4827270585..83304ef6cf 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -166,7 +166,7 @@ class AvatarGalleryController: ViewController { strongSelf.entries = entries strongSelf.centralEntryIndex = 0 if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: entry, delete: strongSelf.peer.id == strongSelf.account.peerId ? { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(account: account, peer: peer, strings: presentationData.strings, entry: entry, delete: strongSelf.peer.id == strongSelf.account.peerId ? { self?.deleteEntry(entry) } : nil) }), centralItemIndex: 0, keepFirst: true) @@ -302,7 +302,7 @@ class AvatarGalleryController: ViewController { } let presentationData = self.presentationData - self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(account: self.account, strings: presentationData.strings, entry: entry, delete: self.peer.id == self.account.peerId ? { [weak self] in + self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(account: self.account, peer: peer, strings: presentationData.strings, entry: entry, delete: self.peer.id == self.account.peerId ? { [weak self] in self?.deleteEntry(entry) } : nil) }), centralItemIndex: self.centralEntryIndex) diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index d6494b8fa6..f43f1940a5 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -151,7 +151,7 @@ public final class AvatarNode: ASDisplayNode { self.displaySuspended = true self.contents = nil - if let signal = peerAvatarImage(account: account, representation: representation) { + if let signal = peerAvatarImage(account: account, peer: peer, representation: representation) { self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) } else { diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift index c2d6537d80..eabf855f78 100644 --- a/TelegramUI/CallControllerNode.swift +++ b/TelegramUI/CallControllerNode.swift @@ -142,8 +142,13 @@ final class CallControllerNode: ASDisplayNode { func updatePeer(peer: Peer) { if !arePeersEqual(self.peer, peer) { self.peer = peer - - self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.account, representations: peer.profileImageRepresentations, autoFetchFullSize: true)) + let representations: [(TelegramMediaImageRepresentation, MediaResourceReference)] + if let peerReference = PeerReference(peer) { + representations = peer.profileImageRepresentations.map({ ($0, .avatar(peer: peerReference, resource: $0.resource)) }) + } else { + representations = [] + } + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.account, representations: representations, autoFetchFullSize: true)) self.statusNode.title = peer.displayTitle diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift index 2b1239b3b9..9be208f5d2 100644 --- a/TelegramUI/CallListCallItem.swift +++ b/TelegramUI/CallListCallItem.swift @@ -212,7 +212,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { self.typeIconNode.displaysAsynchronously = false self.infoButtonNode = HighlightableButtonNode() - self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0) + self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: 6.0, left: 6.0, bottom: 6.0, right: 10.0) super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index a4095d5150..fbdf2f159d 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -547,7 +547,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } })) }, addAdmin: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, excludeAccountPeer: true, openPeer: { peer, participant in + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, mode: .promote, openPeer: { peer, participant in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } if peer.id == account.peerId { return diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index 795d904338..9e819125a5 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -316,7 +316,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View } } }, addPeer: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, excludeAccountPeer: true, openPeer: { peer, participant in + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, mode: .ban, openPeer: { peer, participant in if let participant = participant { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } switch participant.participant { diff --git a/TelegramUI/ChannelMemberCategoryListContext.swift b/TelegramUI/ChannelMemberCategoryListContext.swift index 8701e97168..86bb94e0b6 100644 --- a/TelegramUI/ChannelMemberCategoryListContext.swift +++ b/TelegramUI/ChannelMemberCategoryListContext.swift @@ -3,10 +3,10 @@ import TelegramCore import Postbox import SwiftSignalKit -private let initialBatchSize: Int32 = 32 +private let initialBatchSize: Int32 = 64 private let emptyTimeout: Double = 2.0 * 60.0 private let headUpdateTimeout: Double = 30.0 -private let requestBatchSize: Int32 = 32 +private let requestBatchSize: Int32 = 64 enum ChannelMemberListLoadingState: Equatable { case loading @@ -509,13 +509,15 @@ private final class PeerChannelMemberContextWithSubscribers { emptyTimer.start() } - func subscribe(updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { + func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { let wasEmpty = self.subscribers.isEmpty let index = self.subscribers.add(updated) updated(self.context.listStateValue) if wasEmpty { self.emptyTimer?.invalidate() - self.context.forceUpdateHead() + if requestUpdate { + self.context.forceUpdateHead() + } } return ActionDisposable { [weak self] in Queue.mainQueue().async { @@ -545,10 +547,10 @@ final class PeerChannelMemberCategoriesContext { self.becameEmpty = becameEmpty } - func getContext(key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + func getContext(key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { assert(Queue.mainQueue().isCurrent()) if let current = self.contexts[key] { - return (current.subscribe(updated: updated), PeerChannelMemberCategoryControl(key: key)) + return (current.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) } let context: ChannelMemberCategoryListContext switch key { @@ -575,7 +577,7 @@ final class PeerChannelMemberCategoriesContext { } }) self.contexts[key] = contextWithSubscribers - return (contextWithSubscribers.subscribe(updated: updated), PeerChannelMemberCategoryControl(key: key)) + return (contextWithSubscribers.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) } func loadMore(_ control: PeerChannelMemberCategoryControl) { diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 3d1c8973f3..421724cdd3 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -33,7 +33,7 @@ private enum ChannelMembersSearchSection { private enum ChannelMembersSearchContent: Equatable { case peer(Peer) - case participant(RenderedChannelParticipant) + case participant(RenderedChannelParticipant, String?, Bool) static func ==(lhs: ChannelMembersSearchContent, rhs: ChannelMembersSearchContent) -> Bool { switch lhs { @@ -43,8 +43,8 @@ private enum ChannelMembersSearchContent: Equatable { } else { return false } - case let .participant(participant): - if case .participant(participant) = rhs { + case let .participant(participant, label, enabled): + if case .participant(participant, label, enabled) = rhs { return true } else { return false @@ -56,7 +56,7 @@ private enum ChannelMembersSearchContent: Equatable { switch self { case let .peer(peer): return peer.id - case let .participant(participant): + case let .participant(participant, _, _): return participant.peer.id } } @@ -91,8 +91,14 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in peerSelected(peer, nil) }) - case let .participant(participant): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: participant.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + case let .participant(participant, label, enabled): + let status: ContactsPeerItemStatus + if let label = label { + status = .custom(label) + } else { + status = .none + } + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: participant.peer, status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in peerSelected(participant.peer, participant) }) } @@ -216,7 +222,16 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod case .searchMembers: section = .none } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant), section: section)) + + var label: String? + var enabled = true + if case .banAndPromoteActions = mode { + if case .creator = participant.participant { + label = themeAndStrings.1.Channel_Management_LabelCreator + enabled = false + } + } + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant, label, enabled), section: section)) index += 1 } } @@ -231,7 +246,16 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod case .searchMembers: section = .none } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant), section: section)) + + var label: String? + var enabled = true + if case .banAndPromoteActions = mode { + if case .creator = participant.participant { + label = themeAndStrings.1.Channel_Management_LabelCreator + enabled = false + } + } + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant, label, enabled), section: section)) index += 1 } } diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index 1c6b9524f9..b277fb0167 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -4,12 +4,17 @@ import TelegramCore import Postbox import SwiftSignalKit +enum ChannelMembersSearchControllerMode { + case promote + case ban +} + final class ChannelMembersSearchController: ViewController { private let queue = Queue() private let account: Account private let peerId: PeerId - private let excludeAccountPeer: Bool + private let mode: ChannelMembersSearchControllerMode private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private var presentationData: PresentationData @@ -20,10 +25,10 @@ final class ChannelMembersSearchController: ViewController { return self.displayNode as! ChannelMembersSearchControllerNode } - init(account: Account, peerId: PeerId, excludeAccountPeer: Bool, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { + init(account: Account, peerId: PeerId, mode: ChannelMembersSearchControllerMode, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { self.account = account self.peerId = peerId - self.excludeAccountPeer = excludeAccountPeer + self.mode = mode self.openPeer = openPeer self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -47,7 +52,7 @@ final class ChannelMembersSearchController: ViewController { } override func loadDisplayNode() { - self.displayNode = ChannelMembersSearchControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peerId: self.peerId, excludeAccountPeer: self.excludeAccountPeer) + self.displayNode = ChannelMembersSearchControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peerId: self.peerId, mode: self.mode) self.controllerNode.navigationBar = self.navigationBar self.controllerNode.requestActivateSearch = { [weak self] in self?.activateSearch() diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 0e5ad4b53e..60fe01729b 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -22,7 +22,7 @@ private enum ChannelMembersSearchEntryId: Hashable { private enum ChannelMembersSearchEntry: Comparable, Identifiable { case search - case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing) + case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool) var stableId: ChannelMembersSearchEntryId { switch self { @@ -41,8 +41,8 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsParticipant, lhsEditing): - if case .peer(lhsIndex, lhsParticipant, lhsEditing) = rhs { + case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled): + if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled) = rhs { return true } else { return false @@ -73,8 +73,14 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { interaction.activateSearch() }) - case let .peer(_, participant, editing): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: editing, index: nil, header: nil, action: { _ in + case let .peer(_, participant, editing, label, enabled): + let status: ContactsPeerItemStatus + if let label = label { + status = .custom(label) + } else { + status = .none + } + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in interaction.openPeer(participant.peer, participant) }) } @@ -101,7 +107,7 @@ private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, class ChannelMembersSearchControllerNode: ASDisplayNode { private let account: Account private let peerId: PeerId - private let excludeAccountPeer: Bool + private let mode: ChannelMembersSearchControllerMode let listNode: ListView var navigationBar: NavigationBar? @@ -121,11 +127,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { private var disposable: Disposable? private var listControl: PeerChannelMemberCategoryControl? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId, excludeAccountPeer: Bool) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId, mode: ChannelMembersSearchControllerMode) { self.account = account self.listNode = ListView() self.peerId = peerId - self.excludeAccountPeer = excludeAccountPeer + self.mode = mode self.themeAndStrings = (theme, strings) @@ -155,12 +161,23 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var index = 0 for participant in state.list { - if excludeAccountPeer { - if participant.peer.id == account.peerId { - continue - } + var label: String? + var enabled = true + switch mode { + case .ban: + if participant.peer.id == account.peerId { + continue + } + case .promote: + if participant.peer.id == account.peerId { + continue + } + if case .creator = participant.participant { + label = strings.Channel_Management_LabelCreator + enabled = false + } } - entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false))) + entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) index += 1 } diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index d987628632..214d15c8eb 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -94,14 +94,14 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { let buttonSize = self.button.measure(CGSize(width: width - 80.0, height: 100.0)) - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 45.0 + return panelHeight } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index b7977478f5..8ce8a40ccf 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -129,7 +129,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 let buttonSize = self.button.bounds.size self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) @@ -137,7 +137,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 45.0 + return panelHeight } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/TelegramUI/ChatContextResultPeekContentNode.swift b/TelegramUI/ChatContextResultPeekContentNode.swift index d1840632cb..be500bfe74 100644 --- a/TelegramUI/ChatContextResultPeekContentNode.swift +++ b/TelegramUI/ChatContextResultPeekContentNode.swift @@ -137,7 +137,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var imageResource: TelegramMediaResource? - var videoFile: TelegramMediaFile? + var videoFileReference: FileMediaReference? var imageDimensions: CGSize? switch self.contextResult { case let .externalReference(_, type, title, _, url, content, thumbnail, _): @@ -149,7 +149,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = content?.dimensions if let content = content, type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), reference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) imageResource = nil } case let .internalReference(_, _, title, _, image, file, _): @@ -169,7 +169,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont if let file = file { if file.isVideo && file.isAnimated { - videoFile = file + videoFileReference = .standalone(media: file) imageResource = nil } } @@ -201,11 +201,11 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont } var updatedVideoFile = false - if let currentVideoFile = currentVideoFile, let videoFile = videoFile { - if !currentVideoFile.isEqual(videoFile) { + if let currentVideoFile = currentVideoFile, let videoFileReference = videoFileReference { + if !currentVideoFile.isEqual(videoFileReference.media) { updatedVideoFile = true } - } else if (currentVideoFile != nil) != (videoFile != nil) { + } else if (currentVideoFile != nil) != (videoFileReference != nil) { updatedVideoFile = true } @@ -213,15 +213,14 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) - //updateImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) - updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, photo: tmpImage) + updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() } } self.currentImageResource = imageResource - self.currentVideoFile = videoFile + self.currentVideoFile = videoFileReference?.media if let imageApply = imageApply { if let updateImageSignal = updateImageSignal { @@ -240,13 +239,13 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont layer.layer.removeFromSuperlayer() } - if let videoFile = videoFile { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, file: videoFile) + if let videoFileReference = videoFileReference { + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference) self.layer.addSublayer(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill self.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, resource: videoFile.resource, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: videoFileReference, resource: videoFileReference.media.resource, layerHolder: layerHolder) self.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index b23baefa62..08506d11e8 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -215,8 +215,41 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } + self.attemptNavigation = { [weak self] action in + if let strongSelf = self { + if let _ = strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + if let strongSelf = self { + strongSelf.stopMediaRecorder() + } + action() + })]), in: .window(.root)) + + return false + } + } + return true + } + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { + for media in message.media { + if let action = media as? TelegramMediaAction { + switch action.action { + case .pinnedMessageUpdated: + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + break + } + } + default: + break + } + return true + } + } + return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in @@ -245,8 +278,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.controllerInteraction?.callPeer(peerId) }, enqueueMessage: { message in self?.sendMessages([message]) - }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { file in - self?.controllerInteraction?.sendSticker(file) + }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference in + self?.controllerInteraction?.sendSticker(fileReference) } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in if let strongSelf = self { strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in @@ -344,7 +377,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } strongSelf.sendMessages([.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } - }, sendSticker: { [weak self] file in + }, sendSticker: { [weak self] fileReference in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -360,7 +393,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: fileReference.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, sendGif: { [weak self] file in if let strongSelf = self { @@ -376,7 +409,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: file.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { @@ -1057,11 +1090,11 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } self.inputActivityDisposable = (self.typingActivityPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] value in - if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { - strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .typingText, isPresent: value) - } - }) + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { + strongSelf.account.updateLocalInputActivity(peerId: peerId, activity: .typingText, isPresent: value) + } + }) self.recordingActivityDisposable = (self.recordingActivityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in @@ -1112,6 +1145,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if !value { strongSelf.saveInterfaceState() strongSelf.raiseToListen?.applicationResignedActive() + + strongSelf.stopMediaRecorder() } } }) @@ -1520,13 +1555,13 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in return transaction.getMessage(messageId) } |> deliverOnMainQueue).start(next: { message in - guard let strongSelf = self else { + guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { return } - strongSelf.presentAttachmentMenu(editingMessage: true) + strongSelf.presentAttachmentMenu(editMediaOptions: options) }) } else { - strongSelf.presentAttachmentMenu(editingMessage: false) + strongSelf.presentAttachmentMenu(editMediaOptions: nil) } } @@ -1548,8 +1583,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } self.chatDisplayNode.updateTypingActivity = { [weak self] in - if let strongSelf = self { - strongSelf.typingActivityPromise.set(Signal.single(true) |> then(Signal.single(false) |> delay(4.0, queue: Queue.mainQueue()))) + if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil { + strongSelf.typingActivityPromise.set(Signal.single(true) + |> then( + Signal.single(false) + |> delay(4.0, queue: Queue.mainQueue()) + )) } } @@ -1671,6 +1710,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.present(c, in: .window(.root), with: a) }), in: .window(.root)) } + }, reportMessages: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.present(peerReportOptionsController(account: strongSelf.account, subject: .messages(messages.map({ $0.id }).sorted()), present: { c, a in + self?.present(c, in: .window(.root), with: a) + }), in: .window(.root)) + } }, deleteMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let messageIds = Set(messages.map { $0.id }) @@ -1718,9 +1763,15 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }) } - }, updateTextInputState: { [weak self] f in + }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEffectiveInputState(f($0.effectiveInputState)) } }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode) + return state.updatedInterfaceState { interfaceState in + + return interfaceState.withUpdatedEffectiveInputState(updatedState) + }.updatedInputMode({ _ in updatedMode }) + }) } }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in if let strongSelf = self { @@ -1772,13 +1823,21 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return state }) } - }, error: { _ in + }, error: { error in guard let strongSelf = self else { return } editingMessage.set(nil) - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + + let text: String + switch error { + case .generic: + text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric + case .restricted: + text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { })]), in: .window(.root)) })) } @@ -1935,25 +1994,38 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId))), fromMessage: nil) } }, beginMediaRecording: { [weak self] isVideo in - if let strongSelf = self { - let hasOngoingCall: Signal - if let signal = strongSelf.account.telegramApplicationContext.hasOngoingCall { - hasOngoingCall = signal - } else { - hasOngoingCall = .single(false) - } - let _ = (hasOngoingCall |> deliverOnMainQueue).start(next: { hasOngoingCall in - if let strongSelf = self { - if hasOngoingCall { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_RecordingDisabledMessage, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } else { - if isVideo { - strongSelf.requestVideoRecorder() + let begin: () -> Void = { + if let strongSelf = self { + let hasOngoingCall: Signal + if let signal = strongSelf.account.telegramApplicationContext.hasOngoingCall { + hasOngoingCall = signal + } else { + hasOngoingCall = .single(false) + } + let _ = (hasOngoingCall |> deliverOnMainQueue).start(next: { hasOngoingCall in + if let strongSelf = self { + if hasOngoingCall { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_RecordingDisabledMessage, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) } else { - strongSelf.requestAudioRecorder(beginWithTone: false) + if isVideo { + strongSelf.requestVideoRecorder() + } else { + strongSelf.requestAudioRecorder(beginWithTone: false) + } } } + }) + } + } + if let strongSelf = self { + authorizeDeviceAccess(to: .microphone(isVideo ? .video : .audio), presentationData: strongSelf.presentationData, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, openSettings: { + self?.account.telegramApplicationContext.applicationBindings.openSettings() + }, { granted in + if granted { + begin() } }) } @@ -2094,7 +2166,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: file.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, unblockPeer: { [weak self] in self?.unblockPeer() @@ -2892,7 +2964,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let message = messages.first, case let .message(desc) = message, let media = desc.media { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state - if let editMessageState = state.editMessageState, case .media(true) = editMessageState.content { + if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, media: media)) } if !desc.text.isEmpty { @@ -2919,7 +2991,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } - private func presentAttachmentMenu(editingMessage: Bool) { + private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?) { let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -2930,7 +3002,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } strongSelf.chatDisplayNode.dismissInput() - if !editingMessage, let bannedRights = (peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendMedia) { + if editMediaOptions == nil, let bannedRights = (peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendMedia) { let banDescription: String if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: strongSelf.presentationInterfaceState.strings, timeFormat: .regular)).0 @@ -2968,9 +3040,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin legacyController.bind(controller: navigationController) legacyController.enableSizeClassSignal = true - let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, editingMessage: editingMessage, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { - self?.presentMediaPicker(fileMode: false, completion: { signals in - if editingMessage { + let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, editMediaOptions: editMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { + self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals in + if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals) } else { self?.enqueueMediaMessages(signals: signals) @@ -2978,8 +3050,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) }, openCamera: { cameraView, menuController in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, saveCapturedPhotos: settings.storeEditedPhotos, sendMessagesWithSignals: { signals in - if editingMessage { + presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: settings.storeEditedPhotos, sendMessagesWithSignals: { signals in + if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals!) } else { self?.enqueueMediaMessages(signals: signals) @@ -2987,13 +3059,13 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }, openFileGallery: { - self?.presentFileMediaPickerOptions(editingMessage: editingMessage) + self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) }, openMap: { - self?.presentMapPicker(editingMessage: editingMessage) + self?.presentMapPicker(editingMessage: editMediaOptions != nil) }, openContacts: { self?.presentContactPicker() }, sendMessagesWithSignals: { [weak self] signals in - if editingMessage { + if editMediaOptions != nil { self?.editMessageMediaWithLegacySignals(signals!) } else { self?.enqueueMediaMessages(signals: signals) @@ -3025,7 +3097,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.presentMediaPicker(fileMode: true, completion: { signals in + strongSelf.presentMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { signals in if editingMessage { self?.editMessageMediaWithLegacySignals(signals) } else { @@ -3052,7 +3124,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin for item in results { if let item = item { let fileId = arc4random64() - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), resource: ICloudFileResource(urlData: item.urlData), previewRepresentations: [], mimeType: guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension), size: item.fileSize, attributes: [.FileName(fileName: item.fileName)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), reference: nil, resource: ICloudFileResource(urlData: item.urlData), previewRepresentations: [], mimeType: guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension), size: item.fileSize, attributes: [.FileName(fileName: item.fileName)]) let message: EnqueueMessage = .message(text: "", attributes: [], media: file, replyToMessageId: replyMessageId, localGroupingKey: nil) messages.append(message) } @@ -3088,7 +3160,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.present(actionSheet, in: .window(.root)) } - private func presentMediaPicker(fileMode: Bool, completion: @escaping ([Any]) -> Void) { + private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any]) -> Void) { let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -3096,7 +3168,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - let _ = legacyAssetPicker(theme: strongSelf.presentationData.theme, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in + let _ = legacyAssetPicker(applicationContext: strongSelf.account.telegramApplicationContext, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in if let strongSelf = self { let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBar.style.style @@ -3107,7 +3179,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) controller.descriptionGenerator = legacyAssetPickerItemGenerator() controller.completionBlock = { [weak legacyController] signals in - if let strongSelf = self, let legacyController = legacyController { + if let legacyController = legacyController { legacyController.dismiss() completion(signals!) } @@ -3395,7 +3467,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) strongSelf.audioRecorderFeedback?.tap() strongSelf.audioRecorderFeedback = nil @@ -3407,7 +3479,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } else if let videoRecorderValue = self.videoRecorderValue { if case .send = action { videoRecorderValue.completeVideo() - self.tempVideoRecorderValue = videoRecorderValue + //self.tempVideoRecorderValue = videoRecorderValue self.videoRecorder.set(.single(nil)) } else { self.videoRecorder.set(.single(nil)) @@ -3469,7 +3541,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin var randomId: Int64 = 0 arc4random_buf(&randomId, 8) - self.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + self.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } } @@ -3911,9 +3983,18 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } let unblockingPeer = self.unblockingPeer unblockingPeer.set(true) - self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: false) |> afterDisposed({ + + var restartBot = false + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + restartBot = true + } + self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: false) + |> afterDisposed({ [weak self] in Queue.mainQueue().async { unblockingPeer.set(false) + if let strongSelf = self, restartBot { + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + } } })).start()) } @@ -4002,7 +4083,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } } else if let navigationController = strongSelf.navigationController as? NavigationController { - navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId) + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always) } case .info: strongSelf.navigationActionDisposable.set((strongSelf.account.postbox.loadedPeerWithId(peerId) @@ -4341,6 +4422,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin var isChannel = false if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { personalPeerName = user.compactDisplayTitle + } else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser { + personalPeerName = user.compactDisplayTitle } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { isChannel = true } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 2db50c1129..3137a0592b 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -45,8 +45,8 @@ public final class ChatControllerInteraction { let clickThroughMessage: () -> Void let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendMessage: (String) -> Void - let sendSticker: (TelegramMediaFile) -> Void - let sendGif: (TelegramMediaFile) -> Void + let sendSticker: (FileMediaReference) -> Void + let sendGif: (FileMediaReference) -> Void let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void let activateSwitchInline: (PeerId?, String) -> Void let openUrl: (String) -> Void @@ -77,7 +77,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index c3b3adb2c8..2d5e5943e0 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -32,6 +32,8 @@ private final class ScrollContainerNode: ASScrollNode { } private struct ChatControllerNodeDerivedLayoutState { + var inputContextPanelsFrame: CGRect + var inputContextPanelsOverMainPanelFrame: CGRect var inputNodeHeight: CGFloat? var upperInputPositionBound: CGFloat? } @@ -347,7 +349,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) { if isEmpty && self.emptyNode == nil { - let emptyNode = ChatEmptyNode() + let emptyNode = ChatEmptyNode(accountPeerId: self.account.peerId) if let (size, insets) = self.validEmptyNodeLayout { emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, size: size, insets: insets, transition: .immediate) } @@ -617,6 +619,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if inputPanelNode !== self.inputPanelNode { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() + let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } dismissedInputPanelNode = self.inputPanelNode immediatelyLayoutInputPanelAndAnimateAppearance = true @@ -986,9 +989,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) } - transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) - transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.navigateButtons, frame: navigateButtonsFrame) + var apparentInputPanelFrame = inputPanelFrame + var apparentInputBackgroundFrame = inputBackgroundFrame + var apparentNavigateButtonsFrame = navigateButtonsFrame + if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, let expanded = maybeExpanded, case .search = expanded, let inputPanelFrame = inputPanelFrame { + let verticalOffset = -inputPanelFrame.height - 41.0 + apparentInputPanelFrame = inputPanelFrame.offsetBy(dx: 0.0, dy: verticalOffset) + apparentInputBackgroundFrame.size.height -= verticalOffset + apparentInputBackgroundFrame.origin.y += verticalOffset + apparentNavigateButtonsFrame.origin.y += verticalOffset + } + + transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { if immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance { @@ -997,13 +1011,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateFrame(node: titleAccessoryPanelNode, frame: titleAccessoryPanelFrame) } - if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) { + if let inputPanelNode = self.inputPanelNode, let apparentInputPanelFrame = apparentInputPanelFrame, !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { if immediatelyLayoutInputPanelAndAnimateAppearance { - inputPanelNode.frame = inputPanelFrame.offsetBy(dx: 0.0, dy: inputPanelFrame.size.height) + inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height) inputPanelNode.alpha = 0.0 } - transition.updateFrame(node: inputPanelNode, frame: inputPanelFrame) + transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) } @@ -1025,9 +1039,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputContextPanelNode = self.inputContextPanelNode { let panelFrame = inputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if immediatelyLayoutInputContextPanelAndAnimateAppearance { - inputContextPanelNode.frame = panelFrame - inputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) - } else if !inputContextPanelNode.frame.equalTo(panelFrame) { + var startPanelFrame = panelFrame + if let derivedLayoutState = self.derivedLayoutState { + let referenceFrame = inputContextPanelNode.placement == .overTextInput ? derivedLayoutState.inputContextPanelsOverMainPanelFrame : derivedLayoutState.inputContextPanelsFrame + startPanelFrame.origin.y = referenceFrame.maxY - panelFrame.height + } + inputContextPanelNode.frame = startPanelFrame + inputContextPanelNode.updateLayout(size: startPanelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + } + + if !inputContextPanelNode.frame.equalTo(panelFrame) { transition.updateFrame(node: inputContextPanelNode, frame: panelFrame) inputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) } @@ -1209,7 +1230,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.performAnimateInAsOverlay(from: scheduledAnimateInAsOverlayFromNode, transition: animatedTransition) } - self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) + self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputContextPanelsFrame: inputContextPanelsFrame, inputContextPanelsOverMainPanelFrame: inputContextPanelsOverMainPanelFrame, inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) } private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index 3320a4d24a..cc5daeaad5 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -25,11 +25,11 @@ class ChatDocumentGalleryItem: GalleryItem { for media in self.message.media { if let file = media as? TelegramMediaFile { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let file = content.file { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } } @@ -92,7 +92,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { private let webView: UIView - private var accountAndFile: (Account, TelegramMediaFile)? + private var accountAndFile: (Account, FileMediaReference)? private let dataDisposable = MetaDisposable() private var itemIsVisible = false @@ -168,12 +168,12 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { return .single(.dark) } - func setFile(account: Account, file: TelegramMediaFile) { - let updateFile = self.accountAndFile?.1 != file - self.accountAndFile = (account, file) + func setFile(account: Account, fileReference: FileMediaReference) { + let updateFile = self.accountAndFile?.1.media != fileReference.media + self.accountAndFile = (account, fileReference) if updateFile { self.maybeLoadContent() - self.setupStatus(account: account, resource: file.resource) + self.setupStatus(account: account, resource: fileReference.media.resource) } } @@ -226,12 +226,12 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { } private func maybeLoadContent() { - if let (account, file) = self.accountAndFile { + if let (account, fileReference) = self.accountAndFile { var pathExtension: String? - if let fileName = file.fileName { + if let fileName = fileReference.media.fileName { pathExtension = (fileName as NSString).pathExtension } - let data = account.postbox.mediaBox.resourceData(file.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false)) + let data = account.postbox.mediaBox.resourceData(fileReference.media.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false)) |> deliverOnMainQueue self.dataDisposable.set(data.start(next: { [weak self] data in if let strongSelf = self { @@ -344,12 +344,12 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { } @objc func statusPressed() { - if let (account, file) = self.accountAndFile, let status = self.status { + if let (account, fileReference) = self.accountAndFile, let status = self.status { switch status { case .Fetching: - account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) default: break } diff --git a/TelegramUI/ChatEditInterfaceMessageState.swift b/TelegramUI/ChatEditInterfaceMessageState.swift index ac6c2cc26d..5451515d9e 100644 --- a/TelegramUI/ChatEditInterfaceMessageState.swift +++ b/TelegramUI/ChatEditInterfaceMessageState.swift @@ -4,7 +4,7 @@ import TelegramCore enum ChatEditInterfaceMessageStateContent: Equatable { case plaintext - case media(editable: Bool) + case media(mediaOptions: MessageMediaEditingOptions) } final class ChatEditInterfaceMessageState: Equatable { diff --git a/TelegramUI/ChatEmptyNode.swift b/TelegramUI/ChatEmptyNode.swift index a3e05fd26c..b451ffecf7 100644 --- a/TelegramUI/ChatEmptyNode.swift +++ b/TelegramUI/ChatEmptyNode.swift @@ -186,12 +186,110 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode } } +private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeContent { + private let iconNode: ASImageNode + private let titleNode: ImmediateTextNode + private var lineNodes: [ImmediateTextNode] = [] + + private var currentTheme: PresentationTheme? + private var currentStrings: PresentationStrings? + + override init() { + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + self.titleNode = ImmediateTextNode() + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.lineSpacing = 0.15 + self.titleNode.textAlignment = .center + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + } + + func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { + self.currentTheme = interfaceState.theme + self.currentStrings = interfaceState.strings + + + + let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title + self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) + + let strings: [String] = [ + interfaceState.strings.Conversation_ClousStorageInfo_Description1, + interfaceState.strings.Conversation_ClousStorageInfo_Description2, + interfaceState.strings.Conversation_ClousStorageInfo_Description3, + interfaceState.strings.Conversation_ClousStorageInfo_Description4 + ] + + let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) } + + for i in 0 ..< lines.count { + if i >= self.lineNodes.count { + let textNode = ImmediateTextNode() + textNode.maximumNumberOfLines = 0 + textNode.isLayerBacked = true + textNode.displaysAsynchronously = false + self.addSubnode(textNode) + self.lineNodes.append(textNode) + } + + self.lineNodes[i].attributedText = lines[i] + } + } + + let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + let titleSpacing: CGFloat = 4.0 + + var contentWidth: CGFloat = 100.0 + var contentHeight: CGFloat = 0.0 + + var lineNodes: [(CGSize, ImmediateTextNode)] = [] + for textNode in self.lineNodes { + let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) + contentWidth = max(contentWidth, textSize.width) + contentHeight += textSize.height + titleSpacing + lineNodes.append((textSize, textNode)) + } + + let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) + + contentWidth = max(contentWidth, titleSize.width) + + contentHeight += titleSize.height + titleSpacing + + let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) + + let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + var lineOffset = titleFrame.maxY + titleSpacing + for (textSize, textNode) in lineNodes { + transition.updateFrame(node: textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: lineOffset), size: textSize)) + lineOffset += textSize.height + 4.0 + } + + return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size + } +} + private enum ChatEmptyNodeContentType { case regular case secret + case cloud } final class ChatEmptyNode: ASDisplayNode { + private let accountPeerId: PeerId + private let backgroundNode: ASImageNode private var currentTheme: PresentationTheme? @@ -199,7 +297,9 @@ final class ChatEmptyNode: ASDisplayNode { private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? - override init() { + init(accountPeerId: PeerId) { + self.accountPeerId = accountPeerId + self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true @@ -222,7 +322,9 @@ final class ChatEmptyNode: ASDisplayNode { let contentType: ChatEmptyNodeContentType if let peer = interfaceState.renderedPeer?.peer { - if let _ = peer as? TelegramSecretChat { + if peer.id == self.accountPeerId { + contentType = .cloud + } else if let _ = peer as? TelegramSecretChat { contentType = .secret } else { contentType = .regular @@ -242,6 +344,8 @@ final class ChatEmptyNode: ASDisplayNode { node = ChatEmptyNodeRegularChatContent() case .secret: node = ChatEmptyNodeSecretChatContent() + case .cloud: + node = ChatEmptyNodeCloudChatContent() } self.content = (contentType, node) self.addSubnode(node) diff --git a/TelegramUI/ChatExternalFileGalleryItem.swift b/TelegramUI/ChatExternalFileGalleryItem.swift index 9f9fe3df2e..195075dd0a 100644 --- a/TelegramUI/ChatExternalFileGalleryItem.swift +++ b/TelegramUI/ChatExternalFileGalleryItem.swift @@ -25,11 +25,11 @@ class ChatExternalFileGalleryItem: GalleryItem { for media in self.message.media { if let file = media as? TelegramMediaFile { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let file = content.file { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } } @@ -66,7 +66,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { private let actionTitleNode: ImmediateTextNode private let actionButtonNode: HighlightableButtonNode - private var accountAndFile: (Account, TelegramMediaFile)? + private var accountAndFile: (Account, FileMediaReference)? private let dataDisposable = MetaDisposable() private var itemIsVisible = false @@ -166,12 +166,12 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { return .single(.dark) } - func setFile(account: Account, file: TelegramMediaFile) { - let updateFile = self.accountAndFile?.1 != file - self.accountAndFile = (account, file) + func setFile(account: Account, fileReference: FileMediaReference) { + let updateFile = self.accountAndFile?.1.media != fileReference.media + self.accountAndFile = (account, fileReference) if updateFile { - self.fileNameNode.attributedText = NSAttributedString(string: file.fileName ?? " ", font: Font.regular(17.0), textColor: .black) - self.setupStatus(account: account, resource: file.resource) + self.fileNameNode.attributedText = NSAttributedString(string: fileReference.media.fileName ?? " ", font: Font.regular(17.0), textColor: .black) + self.setupStatus(account: account, resource: fileReference.media.resource) } } @@ -311,12 +311,12 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { } @objc func statusPressed() { - if let (account, file) = self.accountAndFile, let status = self.status { + if let (account, fileReference) = self.accountAndFile, let status = self.status { switch status { case .Fetching: - account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) default: break } diff --git a/TelegramUI/ChatFeedNavigationInputPanelNode.swift b/TelegramUI/ChatFeedNavigationInputPanelNode.swift index 4dfc53cf20..3d45ad1a12 100644 --- a/TelegramUI/ChatFeedNavigationInputPanelNode.swift +++ b/TelegramUI/ChatFeedNavigationInputPanelNode.swift @@ -58,11 +58,11 @@ final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 80.0, height: 100.0)) - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) - return 45.0 + return panelHeight } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 2c1e68555d..7ed5d0d314 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -6,19 +6,19 @@ import Postbox import TelegramCore enum ChatMediaGalleryThumbnail: Equatable { - case image(TelegramMediaImage) - case video(TelegramMediaFile) + case image(ImageMediaReference) + case video(FileMediaReference) static func ==(lhs: ChatMediaGalleryThumbnail, rhs: ChatMediaGalleryThumbnail) -> Bool { switch lhs { case let .image(lhsImage): - if case let .image(rhsImage) = rhs, lhsImage.isEqual(rhsImage) { + if case let .image(rhsImage) = rhs, lhsImage.media.isEqual(rhsImage.media) { return true } else { return false } case let .video(lhsVideo): - if case let .video(rhsVideo) = rhs, lhsVideo.isEqual(rhsVideo) { + if case let .video(rhsVideo) = rhs, lhsVideo.media.isEqual(rhsVideo.media) { return true } else { return false @@ -31,12 +31,12 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { private let account: Account private let thumbnail: ChatMediaGalleryThumbnail - init?(account: Account, media: Media) { + init?(account: Account, mediaReference: AnyMediaReference) { self.account = account - if let media = media as? TelegramMediaImage { - self.thumbnail = .image(media) - } else if let media = media as? TelegramMediaFile, media.isVideo { - self.thumbnail = .video(media) + if let imageReference = mediaReference.concrete(TelegramMediaImage.self) { + self.thumbnail = .image(imageReference) + } else if let fileReference = mediaReference.concrete(TelegramMediaFile.self), fileReference.media.isVideo { + self.thumbnail = .video(fileReference) } else { return nil } @@ -52,15 +52,15 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { switch self.thumbnail { - case let .image(image): - if let representation = largestImageRepresentation(image.representations) { - return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions) + case let .image(imageReference): + if let representation = largestImageRepresentation(imageReference.media.representations) { + return (mediaGridMessagePhoto(account: self.account, photoReference: imageReference), representation.dimensions) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } - case let .video(file): - if let representation = largestImageRepresentation(file.previewRepresentations) { - return (mediaGridMessageVideo(postbox: self.account.postbox, video: file), representation.dimensions) + case let .video(fileReference): + if let representation = largestImageRepresentation(fileReference.media.previewRepresentations) { + return (mediaGridMessageVideo(postbox: self.account.postbox, videoReference: fileReference), representation.dimensions) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } @@ -88,17 +88,17 @@ class ChatImageGalleryItem: GalleryItem { for media in self.message.media { if let image = media as? TelegramMediaImage { - node.setImage(image: image) + node.setImage(imageReference: .message(message: MessageReference(self.message), media: image)) break } else if let file = media as? TelegramMediaFile, file.mimeType.hasPrefix("image/") { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let image = content.image { - node.setImage(image: image) + node.setImage(imageReference: .message(message: MessageReference(self.message), media: image)) break } else if let file = content.file, file.mimeType.hasPrefix("image/") { - node.setFile(account: account, file: file) + node.setFile(account: account, fileReference: .message(message: MessageReference(self.message), media: file)) break } } @@ -123,16 +123,16 @@ class ChatImageGalleryItem: GalleryItem { func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { if let id = self.message.groupInfo?.stableId { - var media: Media? + var mediaReference: AnyMediaReference? for m in self.message.media { if let m = m as? TelegramMediaImage { - media = m + mediaReference = .message(message: MessageReference(self.message), media: m) } else if let m = m as? TelegramMediaFile, m.isVideo { - media = m + mediaReference = .message(message: MessageReference(self.message), media: m) } } - if let media = media { - if let item = ChatMediaGalleryThumbnailItem(account: self.account, media: media) { + if let mediaReference = mediaReference { + if let item = ChatMediaGalleryThumbnailItem(account: self.account, mediaReference: mediaReference) { return (Int64(id), item) } } @@ -152,7 +152,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusNode: RadialStatusNode private let footerContentNode: ChatItemGalleryFooterContentNode - private var accountAndMedia: (Account, Media)? + private var accountAndMedia: (Account, AnyMediaReference)? private var fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() @@ -206,83 +206,84 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setMessage(message) } - fileprivate func setImage(image: TelegramMediaImage) { - if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) { - if let largestSize = largestRepresentationForPhoto(image) { + fileprivate func setImage(imageReference: ImageMediaReference) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(imageReference.media) { + if let largestSize = largestRepresentationForPhoto(imageReference.media) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: imageReference.resourceReference(largestSize.resource)).start()) self.setupStatus(resource: largestSize.resource) } else { self._ready.set(.single(Void())) } } - self.accountAndMedia = (account, image) + self.accountAndMedia = (account, imageReference.abstract) } - func setFile(account: Account, file: TelegramMediaFile) { - if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) { - if let largestSize = file.dimensions { + func setFile(account: Account, fileReference: FileMediaReference) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(fileReference.media) { + if let largestSize = fileReference.media.dimensions { let displaySize = largestSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatMessageImageFile(account: account, file: file, thumbnail: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: account, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) - self.setupStatus(resource: file.resource) + self.setupStatus(resource: fileReference.media.resource) } else { self._ready.set(.single(Void())) } } - self.accountAndMedia = (account, file) + self.accountAndMedia = (account, fileReference.abstract) } private func setupStatus(resource: MediaResource) { self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(resource) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - let previousStatus = strongSelf.status - strongSelf.status = status - switch status { - case .Remote: - strongSelf.statusNode.isHidden = false - strongSelf.statusNode.alpha = 1.0 - strongSelf.statusNodeContainer.isUserInteractionEnabled = true - strongSelf.statusNode.transitionToState(.download(.white), completion: {}) - case let .Fetching(isActive, progress): - strongSelf.statusNode.isHidden = false - strongSelf.statusNode.alpha = 1.0 - strongSelf.statusNodeContainer.isUserInteractionEnabled = true - var actualProgress = progress - if isActive { - actualProgress = max(actualProgress, 0.027) - } - strongSelf.statusNode.transitionToState(.progress(color: .white, value: CGFloat(actualProgress), cancelEnabled: true), completion: {}) - case .Local: - if let previousStatus = previousStatus, case .Fetching = previousStatus { - strongSelf.statusNode.transitionToState(.progress(color: .white, value: 1.0, cancelEnabled: true), completion: { - if let strongSelf = self { - strongSelf.statusNode.alpha = 0.0 - strongSelf.statusNodeContainer.isUserInteractionEnabled = false - strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in - if let strongSelf = self { - strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) - } - }) - } - }) - } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { - strongSelf.statusNode.alpha = 0.0 - strongSelf.statusNodeContainer.isUserInteractionEnabled = false - strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in - if let strongSelf = self { - strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) - } - }) - } - } + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + let previousStatus = strongSelf.status + strongSelf.status = status + switch status { + case .Remote: + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + strongSelf.statusNode.transitionToState(.download(.white), completion: {}) + case let .Fetching(isActive, progress): + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + var actualProgress = progress + if isActive { + actualProgress = max(actualProgress, 0.027) + } + strongSelf.statusNode.transitionToState(.progress(color: .white, value: CGFloat(actualProgress), cancelEnabled: true), completion: {}) + case .Local: + if let previousStatus = previousStatus, case .Fetching = previousStatus { + strongSelf.statusNode.transitionToState(.progress(color: .white, value: 1.0, cancelEnabled: true), completion: { + if let strongSelf = self { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } + }) + } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { + strongSelf.statusNode.alpha = 0.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = false + strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + if let strongSelf = self { + strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) + } + }) + } } - })) + } + })) } override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { @@ -412,9 +413,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) - if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile { + if let (account, mediaReference) = self.accountAndMedia, let fileReference = mediaReference.concrete(TelegramMediaFile.self) { if isVisible { - //self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) } else { self.fetchDisposable.set(nil) } @@ -430,19 +430,22 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } @objc func statusPressed() { - if let (_, media) = self.accountAndMedia, let status = self.status { - var resource: MediaResource? - if let file = media as? TelegramMediaFile { - resource = file.resource - } else if let image = media as? TelegramMediaImage { - resource = largestImageRepresentation(image.representations)?.resource + if let (_, mediaReference) = self.accountAndMedia, let status = self.status { + var resource: MediaResourceReference? + var statsCategory: MediaResourceStatsCategory? + if let fileReference = mediaReference.concrete(TelegramMediaFile.self) { + resource = fileReference.resourceReference(fileReference.media.resource) + statsCategory = statsCategoryForFileWithAttributes(fileReference.media.attributes) + } else if let imageReference = mediaReference.concrete(TelegramMediaImage.self ) { + resource = (largestImageRepresentation(imageReference.media.representations)?.resource).flatMap(imageReference.resourceReference) + statsCategory = .image } if let resource = resource { switch status { case .Fetching: - self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource) + self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) case .Remote: - self.fetchDisposable.set(self.account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: resource, statsCategory: statsCategory ?? .generic).start()) default: break } diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index ff66068d6f..42ecb450a4 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -71,7 +71,7 @@ private func peerButtons(_ peer: Peer, isMuted: Bool) -> [ChatInfoTitleButton] { buttons.append(.info) return buttons } else if let channel = peer as? TelegramChannel { - if channel.flags.contains(.isCreator) { + if channel.flags.contains(.isCreator) || channel.username == nil { return [.search, muteAction, .info] } else { return [.search, .report, muteAction, .info] @@ -80,7 +80,7 @@ private func peerButtons(_ peer: Peer, isMuted: Bool) -> [ChatInfoTitleButton] { if case .creator = group.role { return [.search, muteAction, .info] } else { - return [.search, .report, muteAction, .info] + return [.search, muteAction, .info] } } else { return [.search, muteAction, .info] diff --git a/TelegramUI/ChatInstantVideoMessageDurationNode.swift b/TelegramUI/ChatInstantVideoMessageDurationNode.swift index baf7a10c44..8a69c66f3c 100644 --- a/TelegramUI/ChatInstantVideoMessageDurationNode.swift +++ b/TelegramUI/ChatInstantVideoMessageDurationNode.swift @@ -126,6 +126,14 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { self.updateTimer?.invalidate() } + func updateTheme(textColor: UIColor, fillColor: UIColor) { + if !self.textColor.isEqual(textColor) || !self.fillColor.isEqual(textColor) { + self.textColor = textColor + self.fillColor = fillColor + self.setNeedsDisplay() + } + } + private func ensureHasTimer() { if self.updateTimer == nil { let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 361eb6c1ad..334be95314 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -227,11 +227,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if let _ = chatPresentationInterfaceState.interfaceState.editMessage { return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } else { + var accessoryItems: [ChatTextInputAccessoryItem] = [] + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { + accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) + } if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 { - var accessoryItems: [ChatTextInputAccessoryItem] = [] - if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { - accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) - } var stickersEnabled = true if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { if case .broadcast = peer.info, canSendMessagesToPeer(peer) { @@ -250,10 +250,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup { accessoryItems.append(.inputButtons) } - return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) - } else { - return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } + return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } } } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 2e58227623..a29c1a21b8 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -64,48 +64,64 @@ enum ChatMessageContextMenuAction { case sheet(ChatMessageContextMenuSheetAction) } -func canEditMessageMedia(message: Message) -> Bool { - if message.id.peerId.namespace == Namespaces.Peer.SecretChat { - return false +struct MessageMediaEditingOptions: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue } - if message.groupingKey != nil { - return false + + static let imageOrVideo = MessageMediaEditingOptions(rawValue: 1 << 0) + static let file = MessageMediaEditingOptions(rawValue: 1 << 1) +} + +func messageMediaEditingOptions(message: Message) -> MessageMediaEditingOptions { + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + return [] } for attribute in message.attributes { if attribute is AutoremoveTimeoutMessageAttribute { - return false + return [] } } + + var options: MessageMediaEditingOptions = [] + for media in message.media { if let _ = media as? TelegramMediaImage { - return true + options.formUnion([.imageOrVideo, .file]) } else if let file = media as? TelegramMediaFile { for attribute in file.attributes { switch attribute { case .Sticker: - return false + return [] case .Animated: - return true + return [] case let .Video(video): if video.flags.contains(.instantRoundVideo) { - return false + return [] } else { - return true + options.formUnion([.imageOrVideo, .file]) } case let .Audio(audio): if audio.isVoice { - return false + return [] } else { - return true + options.formUnion([.imageOrVideo, .file]) } default: break } } - return true + options.formUnion([.imageOrVideo, .file]) } } - return false + + if message.groupingKey != nil { + options.remove(.file) + } + + return options } func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState, message: Message) -> ChatPresentationInterfaceState { @@ -126,7 +142,7 @@ func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState if isPlaintext { content = .plaintext } else { - content = .media(editable: canEditMessageMedia(message: message)) + content = .media(mediaOptions: messageMediaEditingOptions(message: message)) } updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(content: content, media: nil)) return updated @@ -359,7 +375,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if file.isVideo { if file.isAnimated { actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_LinkDialogSave, action: { - let _ = addSavedGif(postbox: account.postbox, file: file).start() + let _ = addSavedGif(postbox: account.postbox, fileReference: .message(message: MessageReference(message), media: file)).start() }))) } else if !GlobalExperimentalSettings.isAppStoreBuild { actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: "Stream", action: { @@ -382,6 +398,12 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: }))) } + if data.messageActions.options.contains(.report) { + actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuReport, action: { + interfaceInteraction.reportMessages(messages) + }))) + } + if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .destructive, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, action: { interfaceInteraction.deleteMessages(messages) @@ -424,6 +446,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag optionsMap[id] = [] } if id.peerId == accountPeerId { + optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.deleteLocally) } else if let peer = transaction.getPeer(id.peerId), let message = transaction.getMessage(id) { if let channel = peer as? TelegramChannel { @@ -443,7 +466,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag banPeer = nil } } - if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { optionsMap[id]!.insert(.forward) } if !message.flags.contains(.Incoming) { @@ -454,7 +477,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } } else if let group = peer as? TelegramGroup { - if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { optionsMap[id]!.insert(.forward) if message.flags.contains(.Incoming) { optionsMap[id]!.insert(.report) @@ -472,7 +495,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } } else if let _ = peer as? TelegramUser { - if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia { optionsMap[id]!.insert(.forward) } optionsMap[id]!.insert(.deleteLocally) diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index cfb4a24e9b..513a9b78ff 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -116,7 +116,7 @@ private func updatedContextQueryResultStateForQuery(account: Account, peer: Peer } let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? recentlyUsedInlineBots(postbox: account.postbox) : .single([]) - let participants = combineLatest(inlineBots, searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peer.id, query: query)) + let participants = combineLatest(inlineBots, searchPeerMembers(account: account, peerId: peer.id, query: query)) |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in if rating < 0.14 { diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 5e135f190a..bf00c5aad8 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -122,7 +122,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } switch channel.info { case .broadcast: - if !channel.hasAdminRights([.canPostMessages]) { + if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasAdminRights([.canEditMessages]) { + displayInputTextPanel = true + } else if !channel.hasAdminRights([.canPostMessages]) { if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { return currentPanel } else { diff --git a/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 5bf201c229..b13ac47354 100644 --- a/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -51,7 +51,7 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { - return ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)) + return ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)) } } diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 8e6e1a2d3a..bae49ba6bb 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -187,7 +187,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { func setMessage(_ message: Message) { self.currentMessage = message - self.actionButton.isHidden = message.id.peerId.namespace == Namespaces.Peer.SecretChat + self.actionButton.isHidden = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.containsSecretMedia let canDelete: Bool if let peer = message.peers[message.id.peerId] { @@ -466,7 +466,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { subject = .image(image.representations) } } - let shareController = ShareController(account: strongSelf.account, subject: subject, saveToCameraRoll: true) + let shareController = ShareController(account: strongSelf.account, subject: subject, saveToCameraRoll: saveToCameraRoll) strongSelf.controllerInteraction?.presentController(shareController, nil) } else { var singleText = presentationData.strings.Media_ShareItem(1) diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 53528989e0..a9b94eb58a 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -281,6 +281,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] var canClear = true + var canStop = false if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { canClear = false @@ -288,6 +289,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if let addressName = channel.addressName, !addressName.isEmpty { canClear = false } + } else if let user = peer as? TelegramUser, user.botInfo != nil { + canStop = true } if canClear { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in @@ -307,6 +310,17 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } })) + if canStop { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + if let strongSelf = self { + let _ = removePeerChat(postbox: strongSelf.account.postbox, peerId: peerId, reportChatSpam: false).start() + let _ = requestUpdatePeerIsBlocked(account: strongSelf.account, peerId: peer.id, isBlocked: true).start() + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in @@ -496,11 +510,18 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD func activateSearch() { if self.displayNavigationBar { - if let scrollToTop = self.scrollToTop { - scrollToTop() - } - self.chatListDisplayNode.activateSearch() - self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + let _ = (self.chatListDisplayNode.chatListNode.ready + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + if let scrollToTop = strongSelf.scrollToTop { + scrollToTop() + } + strongSelf.chatListDisplayNode.activateSearch() + strongSelf.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + }) } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index b275539743..9e03fb97e9 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -194,7 +194,7 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem private func leftRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isUnread: Bool) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if isUnread { - options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.ChatList_MarkAsRead, icon: readIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.ChatList_MarkAsRead, icon: readIcon, color: theme.list.itemDisclosureActions.neutral1.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) } else { options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.ChatList_MarkAsUnread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index db5c0623ef..8843a29fa4 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -401,6 +401,24 @@ private struct ChatListSearchContainerNodeState: Equatable { } } +private func doesPeerMatchFilter(peer: Peer, filter: ChatListNodePeersFilter) -> Bool { + var enabled = true + if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { + enabled = false + } + if filter.contains(.onlyUsers), !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + if filter.contains(.onlyGroups) { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } + return enabled +} + final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openMessage: (Peer, MessageId) -> Void @@ -580,8 +598,10 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let recentItemsTransition = combineLatest(hasRecentPeers, recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) |> mapToSignal { [weak self] hasRecentPeers, peers, presentationData, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] - if groupId == nil, hasRecentPeers { - entries.append(.topPeers([], presentationData.theme, presentationData.strings)) + if !filter.contains(.onlyGroups) { + if groupId == nil, hasRecentPeers { + entries.append(.topPeers([], presentationData.theme, presentationData.strings)) + } } var peerIds = Set() var index = 0 @@ -590,6 +610,9 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if peerIds.contains(peer.id) { continue loop } + if !doesPeerMatchFilter(peer: peer, filter: filter) { + continue + } peerIds.insert(peer.id) var associatedPeer: Peer? @@ -644,7 +667,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings + //let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 4127687e2c..e066013351 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -49,7 +49,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } } - func fileAt(point: CGPoint) -> TelegramMediaFile? { + func fileAt(point: CGPoint) -> FileMediaReference? { if let multiplexedNode = self.multiplexedNode { return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY)) } else { @@ -70,13 +70,16 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.view.addSubview(multiplexedNode) let initialOrder = Atomic<[MediaId]?>(value: nil) let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) - |> map { view -> [TelegramMediaFile] in + |> map { view -> [FileMediaReference] in var recentGifs: OrderedItemListView? if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { recentGifs = orderedView as? OrderedItemListView } if let recentGifs = recentGifs { - return recentGifs.items.map { ($0.contents as! RecentMediaItem).media as! TelegramMediaFile } + return recentGifs.items.map { item in + let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile + return .savedGif(media: file) + } } else { return [] } @@ -88,8 +91,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } })) - multiplexedNode.fileSelected = { [weak self] file in - self?.controllerInteraction.sendGif(file) + multiplexedNode.fileSelected = { [weak self] fileReference in + self?.controllerInteraction.sendGif(fileReference) } } } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 899d976c8f..194c9e4fb9 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -24,6 +24,7 @@ private struct ChatMediaInputGridTransition { let updateFirstIndexInSectionOffset: Int? let stationaryItems: GridNodeStationaryItems let scrollToItem: GridNodeScrollToItem? + let updateOpaqueState: ChatMediaInputStickerPaneOpaqueState? let animated: Bool } @@ -37,7 +38,7 @@ private func preparedChatMediaInputPanelEntryTransition(account: Account, from f return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } -private func preparedChatMediaInputGridEntryTransition(account: Account, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition { +private func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition { var stationaryItems: GridNodeStationaryItems = .none var scrollToItem: GridNodeScrollToItem? var animated = false @@ -126,7 +127,9 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr } } - return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) + let opaqueState = ChatMediaInputStickerPaneOpaqueState(hasLower: view.lower != nil) + + return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { @@ -356,6 +359,8 @@ final class ChatMediaInputNode: ChatInputNode { private let trendingPane: ChatMediaInputTrendingPane private var animatingTrendingPaneOut = false + private var panRecognizer: UIPanGestureRecognizer? + private let itemCollectionsViewPosition = Promise() private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? private var currentView: ItemCollectionsView? @@ -580,10 +585,11 @@ final class ChatMediaInputNode: ChatInputNode { let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack, theme: theme) let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) + return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) } - self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in + self.disposable.set((transitions + |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in if let strongSelf = self { strongSelf.currentView = view strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) @@ -682,7 +688,7 @@ final class ChatMediaInputNode: ChatInputNode { menuItems = [ PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { - strongSelf.controllerInteraction.sendSticker(item.file) + strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file)) } }), PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { @@ -706,6 +712,8 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.controllerInteraction.sendSticker(file) } } + + strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) } break loop @@ -730,7 +738,7 @@ final class ChatMediaInputNode: ChatInputNode { if pane.supernode != nil, pane.frame.contains(point) { if let pane = pane as? ChatMediaInputGifPane { if let file = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { - return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(id: "", type: "gif", title: nil, description: nil, image: nil, file: file, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ + return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.controllerInteraction.sendGif(file) @@ -738,7 +746,7 @@ final class ChatMediaInputNode: ChatInputNode { }), PeekControllerMenuItem(title: strongSelf.strings.Common_Delete, color: .destructive, action: { if let strongSelf = self { - let _ = removeSavedGif(postbox: strongSelf.account.postbox, mediaId: file.fileId).start() + let _ = removeSavedGif(postbox: strongSelf.account.postbox, mediaId: file.media.fileId).start() } }) ]))) @@ -762,7 +770,7 @@ final class ChatMediaInputNode: ChatInputNode { menuItems = [ PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { - strongSelf.controllerInteraction.sendSticker(item.file) + strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file)) } }), PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { @@ -786,6 +794,8 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.controllerInteraction.sendSticker(file) } } + + strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) } break loop @@ -826,7 +836,9 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.updatePreviewingItem(item: item, animated: true) } })) - self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.view.addGestureRecognizer(panRecognizer) } private func setCurrentPane(_ pane: ChatMediaInputPaneType, transition: ContainedViewLayoutTransition) { @@ -1002,9 +1014,37 @@ final class ChatMediaInputNode: ChatInputNode { panelHeight = standardInputHeight } + if displaySearch { + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) + if let stickerSearchContainerNode = self.stickerSearchContainerNode { + transition.updateFrame(node: stickerSearchContainerNode, frame: containerFrame) + stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition) + } else { + let stickerSearchContainerNode = StickerPaneSearchContainerNode(account: self.account, theme: self.theme, strings: self.strings, controllerInteraction: self.controllerInteraction, inputNodeInteraction: self.inputNodeInteraction, cancel: { [weak self] in + self?.stickerSearchContainerNode?.deactivate() + self?.inputNodeInteraction.toggleSearch(false) + }) + self.stickerSearchContainerNode = stickerSearchContainerNode + self.addSubnode(stickerSearchContainerNode) + stickerSearchContainerNode.frame = containerFrame + stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate) + var placeholderNode: StickerPaneSearchBarPlaceholderNode? + self.stickerPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode { + placeholderNode = itemNode + } + } + if let placeholderNode = placeholderNode { + stickerSearchContainerNode.animateIn(from: placeholderNode, transition: transition) + } + } + } + + let contentVerticalOffset: CGFloat = displaySearch ? -(inputPanelHeight + 41.0) : 0.0 + let collectionListPanelOffset = self.currentCollectionListPanelOffset() - transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + UIScreenPixel)))) + transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOffset), size: CGSize(width: width, height: max(0.0, 41.0 + UIScreenPixel)))) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) @@ -1054,7 +1094,7 @@ final class ChatMediaInputNode: ChatInputNode { case .gifs: if self.gifPane.supernode == nil { self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) - self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) + self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.gifPane.frame != paneFrame { self.gifPane.layer.removeAnimation(forKey: "position") @@ -1063,7 +1103,7 @@ final class ChatMediaInputNode: ChatInputNode { case .stickers: if self.stickerPane.supernode == nil { self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) - self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) + self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.stickerPane.frame != paneFrame { self.stickerPane.layer.removeAnimation(forKey: "position") @@ -1072,7 +1112,7 @@ final class ChatMediaInputNode: ChatInputNode { case .trending: if self.trendingPane.supernode == nil { self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListContainer) - self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) + self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.trendingPane.frame != paneFrame { self.trendingPane.layer.removeAnimation(forKey: "position") @@ -1160,31 +1200,7 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingTrendingPaneOut = false } - if displaySearch { - let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) - if let stickerSearchContainerNode = self.stickerSearchContainerNode { - transition.updateFrame(node: stickerSearchContainerNode, frame: containerFrame) - stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition) - } else { - let stickerSearchContainerNode = StickerPaneSearchContainerNode(account: self.account, theme: self.theme, strings: self.strings, controllerInteraction: self.controllerInteraction, inputNodeInteraction: self.inputNodeInteraction, cancel: { [weak self] in - self?.stickerSearchContainerNode?.deactivate() - self?.inputNodeInteraction.toggleSearch(false) - }) - self.stickerSearchContainerNode = stickerSearchContainerNode - self.addSubnode(stickerSearchContainerNode) - stickerSearchContainerNode.frame = containerFrame - stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate) - var placeholderNode: StickerPaneSearchBarPlaceholderNode? - self.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } - } - if let placeholderNode = placeholderNode { - stickerSearchContainerNode.animateIn(from: placeholderNode, transition: transition) - } - } - } else if let stickerSearchContainerNode = self.stickerSearchContainerNode { + if !displaySearch, let stickerSearchContainerNode = self.stickerSearchContainerNode { self.stickerSearchContainerNode = nil var placeholderNode: StickerPaneSearchBarPlaceholderNode? @@ -1202,6 +1218,10 @@ final class ChatMediaInputNode: ChatInputNode { } } + if let panRecognizer = self.panRecognizer, panRecognizer.isEnabled != !displaySearch { + panRecognizer.isEnabled = !displaySearch + } + return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) } diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 60d96fbe34..0ee9ec1407 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -168,7 +168,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { if let dimensions = stickerItem.file.dimensions { self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file)).start()) self.currentState = (account, stickerItem, dimensions) self.setNeedsLayout() @@ -195,7 +195,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - interfaceInteraction.sendSticker(item.file) + interfaceInteraction.sendSticker(.standalone(media: item.file)) } } diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index 2091cd200c..878632d845 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -110,7 +110,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: item.file).start()) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file)).start()) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) } diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index ec86aaab2a..3934d0cf3a 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -5,6 +5,14 @@ import Postbox import TelegramCore import SwiftSignalKit +final class ChatMediaInputStickerPaneOpaqueState { + let hasLower: Bool + + init(hasLower: Bool) { + self.hasLower = hasLower + } +} + final class ChatMediaInputStickerPane: ChatMediaInputPane { let gridNode: GridNode private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void @@ -22,8 +30,13 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { self.addSubnode(self.gridNode) self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in - if let strongSelf = self { - let offset = -(layout.contentOffset.y + 41.0) + if let strongSelf = self, let opaqueState = strongSelf.gridNode.opaqueState as? ChatMediaInputStickerPaneOpaqueState { + let offset: CGFloat + if opaqueState.hasLower { + offset = -(layout.contentOffset.y + 41.0) + } else { + offset = -(layout.contentOffset.y + 41.0) + } var relativeChange: CGFloat = 0.0 if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset { relativeChange = offset - didScrollPreviousOffset diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index c51e3f033a..8f7fc656cf 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -132,6 +132,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { self.isActivated = true let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + let theme = presentationData.theme let interaction = TrendingPaneInteraction(installPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { @@ -150,15 +151,19 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { break } return .complete() - }).start() + } |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.controllerInteraction.presentController(OverlayStatusController(theme: theme, type: .success), nil) + } + }) } }, openPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { strongSelf.view.window?.endEditing(true) let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()) - controller.sendSticker = { file in + controller.sendSticker = { fileReference in if let strongSelf = self { - strongSelf.controllerInteraction.sendSticker(file) + strongSelf.controllerInteraction.sendSticker(fileReference) } } strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 71f3bf7d24..108cfc9f02 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -259,7 +259,13 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P case .historyCleared: break case .historyScreenshot: - attributedString = NSAttributedString(string: strings.Notification_SecretChatScreenshot, font: titleFont, textColor: primaryTextColor) + let text: String + if message.effectivelyIncoming(accountPeerId) { + text = strings.Notification_SecretChatMessageScreenshot(message.author?.compactDisplayTitle ?? "").0 + } else { + text = strings.Notification_SecretChatMessageScreenshotSelf + } + attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor) case let .gameScore(gameId: _, score): var gameTitle: String? inner: for attribute in message.attributes { @@ -451,17 +457,6 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { override func didLoad() { super.didLoad() - - /*let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } - recognizer.highlight = { [weak self] point in - if let strongSelf = self { - strongSelf.updateTouchesAtPoint(point) - } - } - self.view.addGestureRecognizer(recognizer)*/ } override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { @@ -500,7 +495,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let backgroundApply = backgroundLayout(item.presentationData.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) - var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) + let layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation in @@ -520,102 +515,6 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .began: - break - case .ended: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - var foundTapAction = false - let tapAction = self.tapActionAtPoint(location) - switch tapAction { - case .none, .ignore: - break - case let .url(url): - foundTapAction = true - self.item?.controllerInteraction.openUrl(url) - case let .peerMention(peerId, _): - foundTapAction = true - self.item?.controllerInteraction.openPeer(peerId, .info, nil) - case let .textMention(name): - foundTapAction = true - self.item?.controllerInteraction.openPeerMention(name) - case let .botCommand(command): - foundTapAction = true - if let item = self.item { - item.controllerInteraction.sendBotCommand(item.message.id, command) - } - case let .hashtag(peerName, hashtag): - foundTapAction = true - self.item?.controllerInteraction.openHashtag(peerName, hashtag) - case .instantPage: - foundTapAction = true - if let item = self.item { - item.controllerInteraction.openInstantPage(item.message) - } - case let .call(peerId): - foundTapAction = true - self.item?.controllerInteraction.callPeer(peerId) - } - if !foundTapAction { - if let item = self.item { - for attribute in item.message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) - foundTapAction = true - break - } - } - } - } - if !foundTapAction { - self.item?.controllerInteraction.clickThroughMessage() - } - case .longTap, .doubleTap: - if let item = self.item, self.labelNode.frame.contains(location) { - var foundTapAction = false - let tapAction = self.tapActionAtPoint(location) - switch tapAction { - case .none, .ignore: - break - case let .url(url): - foundTapAction = true - item.controllerInteraction.longTap(.url(url)) - case let .peerMention(peerId, mention): - foundTapAction = true - item.controllerInteraction.longTap(.peerMention(peerId, mention)) - case let .textMention(name): - foundTapAction = true - item.controllerInteraction.longTap(.mention(name)) - case let .botCommand(command): - foundTapAction = true - item.controllerInteraction.longTap(.command(command)) - case let .hashtag(_, hashtag): - foundTapAction = true - item.controllerInteraction.longTap(.hashtag(hashtag)) - case .instantPage: - break - case .call: - break - } - - if !foundTapAction { - item.controllerInteraction.openMessageContextMenu(item.message, self, self.filledBackgroundNode.frame) - } - } - case .hold: - break - } - } - case .cancelled: - break - default: - break - } - } - override func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? @@ -680,9 +579,11 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { return .botCommand(botCommand) } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) - } else { - return .none } + } + + if self.filledBackgroundNode.frame.contains(point.offsetBy(dx: 0.0, dy: -10.0)) { + return .openMessage } else { return .none } diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index 1435e6da1b..c40fd64b1e 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -212,6 +212,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { private let textNode: TextNode private let inlineImageNode: TransformImageNode private var contentImageNode: ChatMessageInteractiveMediaNode? + private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? private var contentFileNode: ChatMessageInteractiveFileNode? private var buttonBackgroundNode: ASImageNode? private var buttonNode: ChatMessageAttachedContentButtonNode? @@ -231,6 +232,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var visibility: ListViewItemNodeVisibility = .none { didSet { self.contentImageNode?.visibility = self.visibility + self.contentInstantVideoNode?.visibility = self.visibility } } @@ -261,19 +263,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: AutomaticMediaDownloadSettings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: AutomaticMediaDownloadSettings, _ account: Account, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let textAsyncLayout = TextNode.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() let statusLayout = self.statusNode.asyncLayout() let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) + let contentInstantVideoLayout = ChatMessageInteractiveInstantVideoNode.asyncLayout(self.contentInstantVideoNode) let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - return { presentationData, automaticDownloadSettings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + return { presentationData, automaticDownloadSettings, account, controllerInteraction, message, messageRead, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in let incoming = message.effectivelyIncoming(account.peerId) var horizontalInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) @@ -329,6 +332,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude var refineContentImageLayout: ((CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode)))? var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))? + + var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)? let string = NSMutableAttributedString() var notEmpty = false @@ -367,12 +372,21 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { - if file.isVideo { + if file.isInstantVideo { + let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(account: account, controllerInteraction: controllerInteraction, message: message, read: messageRead, presentationData: presentationData), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 180.0, height: 180.0), .bubble) + initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight + contentInstantVideoSizeAndApply = (videoLayout, apply) + } else if file.isVideo { var automaticDownload = false automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: file) let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout + } else if file.isSticker, let _ = file.dimensions { + let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: file) + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) + initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right + refineContentImageLayout = refineLayout } else { var automaticDownload = false automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: file) @@ -403,7 +417,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { inlineImageDimensions = dimensions if image != currentImage { - updateInlineImageSignal = chatWebpageSnippetPhoto(account: account, photo: image) + updateInlineImageSignal = chatWebpageSnippetPhoto(account: account, photoReference: .message(message: MessageReference(message), media: image)) } } } else if let image = media as? TelegramMediaWebFile { @@ -441,7 +455,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { switch position { case .linear(_, .None): - let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText) + let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText) statusInText = !imageMode var skipStandardStatus = false @@ -555,6 +569,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { boundingSize.width = max(boundingSize.width, refinedWidth) } + if let (videoLayout, _) = contentInstantVideoSizeAndApply { + boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight) + } + lineHeight += insets.top + insets.bottom var imageApply: (() -> Void)? @@ -640,6 +658,16 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedLineHeight += imageHeigthAddition + 4.0 } + if let (videoLayout, _) = contentInstantVideoSizeAndApply { + let imageHeigthAddition = videoLayout.contentSize.height + 6.0 + if textFrame.size.height > CGFloat.ulpOfOne { + //imageHeigthAddition += 2.0 + } + + adjustedBoundingSize.height += imageHeigthAddition// + 5.0 + adjustedLineHeight += imageHeigthAddition// + 4.0 + } + var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? if let continueActionButtonLayout = continueActionButtonLayout { let (size, apply) = continueActionButtonLayout(boundingWidth - 13.0 - insets.right) @@ -754,7 +782,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } } - let _ = contentFileApply() if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) } else { @@ -765,6 +792,23 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.contentFileNode = nil } + if let (videoLayout, apply) = contentInstantVideoSizeAndApply { + contentMediaHeight = videoLayout.contentSize.height + let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), transition) + if strongSelf.contentInstantVideoNode !== contentInstantVideoNode { + strongSelf.contentInstantVideoNode = contentInstantVideoNode + strongSelf.addSubnode(contentInstantVideoNode) + } + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize) + } else { + contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize) + } + } else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode { + contentInstantVideoNode.removeFromSupernode() + strongSelf.contentInstantVideoNode = nil + } + var textVerticalOffset: CGFloat = 0.0 if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { textVerticalOffset = contentMediaHeight + 7.0 diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 88da21dca9..ea5b24b43b 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -71,6 +71,7 @@ enum ChatMessageBubbleContentTapAction { case hashtag(String?, String) case instantPage case call(PeerId) + case openMessage case ignore } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index dd06b2550f..37e250fc4d 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -232,7 +232,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break case .ignore: return .fail - case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .call: + case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .call, .openMessage: return .waitForSingleTap } } @@ -1538,6 +1538,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { foundTapAction = true self.item?.controllerInteraction.callPeer(peerId) break loop + case .openMessage: + foundTapAction = true + if let item = self.item { + let _ = item.controllerInteraction.openMessage(item.message) + } + break loop } } if !foundTapAction { @@ -1580,6 +1586,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break case .call: break + case .openMessage: + foundTapAction = true + break } } if !foundTapAction, let tapMessage = tapMessage { @@ -1706,7 +1715,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return } - if let selectionState = item.controllerInteraction.selectionState { + var canHaveSelection = true + switch item.content { + case let .message(message, _, _, _): + for media in message.media { + if media is TelegramMediaAction { + canHaveSelection = false + break + } + } + default: + break + } + + if let selectionState = item.controllerInteraction.selectionState, canHaveSelection { var selected = false var incoming = true diff --git a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift index e52a2c1afc..8462ed8c02 100644 --- a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -43,7 +43,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift index 50a654bd80..8f992e7c9d 100644 --- a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift @@ -38,7 +38,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent let text: String = item.message.text let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift index 982bf63a84..0125c37c40 100644 --- a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift @@ -43,7 +43,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index f191fc20ca..56bba8c0ab 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -65,7 +65,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index da0084ed2a..36a2e01d65 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -6,21 +6,14 @@ import Postbox import TelegramCore class ChatMessageInstantVideoItemNode: ChatMessageItemView { - private var videoNode: UniversalVideoNode? + private let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? - private var statusNode: RadialStatusNode? - private var playbackStatusNode: InstantVideoRadialStatusNode? - private var videoFrame: CGRect? - private var selectionNode: ChatMessageSelectionNode? private var appliedItem: ChatMessageItem? - var telegramFile: TelegramMediaFile? - - private let fetchDisposable = MetaDisposable() private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardBackgroundNode: ASImageNode? @@ -28,63 +21,28 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? - private var durationNode: ChatInstantVideoMessageDurationNode? - private let dateAndStatusNode: ChatMessageDateAndStatusNode - - private let infoBackgroundNode: ASImageNode - private let muteIconNode: ASImageNode - - private var status: FileMediaResourceStatus? - private let playbackStatusDisposable = MetaDisposable() - private var currentSwipeToReplyTranslation: CGFloat = 0.0 - private var shouldAcquireVideoContext: Bool { - if case .visible = self.visibility { - return true - } else { - return false - } - } - override var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { - self.videoNode?.canAttachContent = self.shouldAcquireVideoContext - //self.hostedVideoNode?.setShouldAcquireContext(self.shouldAcquireVideoContext) + self.interactiveVideoNode.visibility = self.visibility } } } required init() { - self.infoBackgroundNode = ASImageNode() - self.infoBackgroundNode.isLayerBacked = true - self.infoBackgroundNode.displayWithoutProcessing = true - self.infoBackgroundNode.displaysAsynchronously = false - - self.dateAndStatusNode = ChatMessageDateAndStatusNode() - - self.muteIconNode = ASImageNode() - self.muteIconNode.isLayerBacked = true - self.muteIconNode.displayWithoutProcessing = true - self.muteIconNode.displaysAsynchronously = false + self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() super.init(layerBacked: false) - self.addSubnode(self.dateAndStatusNode) - self.addSubnode(self.infoBackgroundNode) - self.infoBackgroundNode.addSubnode(self.muteIconNode) + self.addSubnode(self.interactiveVideoNode) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - self.fetchDisposable.dispose() - self.playbackStatusDisposable.dispose() - } - override func didLoad() { super.didLoad() @@ -108,10 +66,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let displaySize = CGSize(width: 212.0, height: 212.0) - let previousFile = self.telegramFile let layoutConstants = self.layoutConstants + let makeVideoLayout = self.interactiveVideoNode.asyncLayout() + let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentReplyBackgroundNode = self.replyBackgroundNode @@ -120,80 +78,27 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let currentItem = self.appliedItem - let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() - return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - var updatedTheme: PresentationTheme? - - var updatedInfoBackgroundImage: UIImage? - var updatedMuteIconImage: UIImage? - if item.presentationData.theme !== currentItem?.presentationData.theme { - updatedTheme = item.presentationData.theme - updatedInfoBackgroundImage = PresentationResourcesChat.chatInstantMessageInfoBackgroundImage(item.presentationData.theme) - updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme) - } - - let instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme) - - let theme = item.presentationData.theme - let isSecretMedia = item.message.containsSecretMedia - let incoming = item.message.effectivelyIncoming(item.account.peerId) - let imageSize = displaySize - - var updatedFile: TelegramMediaFile? - var updatedMedia = false - for media in item.message.media { - if let file = media as? TelegramMediaFile { - updatedFile = file - if let previousFile = previousFile { - updatedMedia = !previousFile.isEqual(file) - } else if previousFile == nil { - updatedMedia = true - } - } - } - - var notConsumed = false - for attribute in item.message.attributes { - if let attribute = attribute as? ConsumableContentMessageAttribute { - if !attribute.consumed { - notConsumed = true - } - break - } - } - - var updatedPlaybackStatus: Signal? - if let updatedFile = updatedFile, updatedMedia { - updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) - |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in - if let pendingStatus = pendingStatus { - var progress = pendingStatus.progress - if pendingStatus.isRunning { - progress = max(progress, 0.27) - } - return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)) - } else { - return resourceStatus - } - } - } let avatarInset: CGFloat var hasAvatar = false switch item.chatLocation { case let .peer(peerId): - if peerId.isGroupOrChannel && item.message.author != nil { - var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true - } - - if !isBroadcastChannel { - hasAvatar = true + if peerId != item.account.peerId { + if peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } } + } else if incoming { + hasAvatar = true } case .group: hasAvatar = true @@ -210,16 +115,18 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { layoutInsets.top += layoutConstants.timestampHeaderHeight } - let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: imageSize) + let displaySize = CGSize(width: 212.0, height: 212.0) - let arguments = TransformImageArguments(corners: ImageCorners(radius: videoFrame.size.width / 2.0), imageSize: videoFrame.size, boundingSize: videoFrame.size, intrinsicInsets: UIEdgeInsets()) + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(account: item.account, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, presentationData: item.presentationData), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free) + + let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: videoLayout.contentSize) var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? var updatedReplyBackgroundNode: ASImageNode? var replyBackgroundImage: UIImage? for attribute in item.message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { - let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - videoLayout.contentSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) replyInfoApply = makeReplyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentReplyBackgroundNode = currentReplyBackgroundNode { @@ -232,6 +139,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } + let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left + var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode)? var updatedForwardBackgroundNode: ASImageNode? var forwardBackgroundImage: UIImage? @@ -252,7 +161,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { forwardSource = forwardInfo.author forwardAuthorSignature = nil } - let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width + 6.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) + let availableWidth = max(60.0, availableContentWidth - videoLayout.contentSize.width + 6.0) forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentForwardBackgroundNode = currentForwardBackgroundNode { @@ -264,239 +173,24 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { forwardBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.presentationData.theme) } - let statusType: ChatMessageDateAndStatusType - if item.message.effectivelyIncoming(item.account.peerId) { - statusType = .FreeIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .FreeOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .FreeOutgoing(.Sending) - } else { - statusType = .FreeOutgoing(.Sent(read: item.read)) - } - } - - var edited = false - var sentViaBot = false - var viewCount: Int? - for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute { - edited = true - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let _ = attribute as? InlineBotMessageAttribute { - sentViaBot = true - } - } - if let author = item.message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true - } - - let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) - - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) - - return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: videoLayout.contentSize.height), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item - strongSelf.videoFrame = videoFrame - - if let updatedInfoBackgroundImage = updatedInfoBackgroundImage { - strongSelf.infoBackgroundNode.image = updatedInfoBackgroundImage + + let transition: ContainedViewLayoutTransition + if animation.isAnimated { + transition = .animated(duration: 0.2, curve: .spring) + } else { + transition = .immediate } - - if let updatedMuteIconImage = updatedMuteIconImage { - strongSelf.muteIconNode.image = updatedMuteIconImage - } - - strongSelf.telegramFile = updatedFile - - if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image { - let infoWidth = muteImage.size.width - let transition: ContainedViewLayoutTransition - if animation.isAnimated { - transition = .animated(duration: 0.2, curve: .spring) - } else { - transition = .immediate - } - let infoBackgroundFrame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - infoWidth) / 2.0), y: videoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height)) - transition.updateFrame(node: strongSelf.infoBackgroundNode, frame: infoBackgroundFrame) - let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size) - transition.updateFrame(node: strongSelf.muteIconNode, frame: muteIconFrame) - } - - if let updatedPlaybackStatus = updatedPlaybackStatus { - strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in - if let strongSelf = self, let videoFrame = strongSelf.videoFrame { - strongSelf.status = status - - let displayMute: Bool - switch status { - case let .fetchStatus(fetchStatus): - switch fetchStatus { - case .Local: - displayMute = true - default: - displayMute = false - } - case .playbackStatus: - displayMute = false - } - if displayMute != (!strongSelf.infoBackgroundNode.alpha.isZero) { - if displayMute { - strongSelf.infoBackgroundNode.alpha = 1.0 - strongSelf.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - strongSelf.infoBackgroundNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15) - } else { - strongSelf.infoBackgroundNode.alpha = 0.0 - strongSelf.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) - strongSelf.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) - } - } - - var progressRequired = false - if case let .fetchStatus(fetchStatus) = status { - if case .Local = fetchStatus { - if let file = updatedFile, file.isVideo { - progressRequired = true - } else if isSecretMedia { - progressRequired = true - } - } else { - progressRequired = true - } - } - - if progressRequired { - if strongSelf.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) - statusNode.isUserInteractionEnabled = false - statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) - strongSelf.statusNode = statusNode - strongSelf.addSubnode(statusNode) - } else if let _ = updatedTheme { - - //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) - } - } else { - if let statusNode = strongSelf.statusNode { - statusNode.transitionToState(.none, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - strongSelf.statusNode = nil - } - } - - var state: RadialStatusNodeState - let bubbleTheme = theme.chat.bubble - switch status { - case let .fetchStatus(fetchStatus): - switch fetchStatus { - case let .Fetching(isActive, progress): - var adjustedProgress = progress - if isActive { - adjustedProgress = max(adjustedProgress, 0.027) - } - state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) - case .Local: - state = .none - /*if isSecretMedia && secretProgressIcon != nil { - state = .customIcon(secretProgressIcon!) - } else */ - case .Remote: - state = .download(bubbleTheme.mediaOverlayControlForegroundColor) - } - default: - state = .none - break - } - if let statusNode = strongSelf.statusNode { - if state == .none { - strongSelf.statusNode = nil - } - statusNode.transitionToState(state, completion: { [weak statusNode] in - if state == .none { - statusNode?.removeFromSupernode() - } - }) - } - - if case .playbackStatus = status { - let playbackStatusNode: InstantVideoRadialStatusNode - if let current = strongSelf.playbackStatusNode { - playbackStatusNode = current - } else { - playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) - strongSelf.addSubnode(playbackStatusNode) - strongSelf.playbackStatusNode = playbackStatusNode - } - playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5) - if let updatedFile = updatedFile { - let status = messageFileMediaPlaybackStatus(account: item.account, file: updatedFile, message: item.message) - playbackStatusNode.status = status - strongSelf.durationNode?.status = status |> map(Optional.init) - } - } else { - if let playbackStatusNode = strongSelf.playbackStatusNode { - strongSelf.playbackStatusNode = nil - playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackStatusNode] _ in - playbackStatusNode?.removeFromSupernode() - }) - } - - strongSelf.durationNode?.status = .single(nil) - } - } - })) - } - - dateAndStatusApply(false) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 55.0, params.width - params.rightInset - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) - - if let telegramFile = updatedFile, updatedMedia { - let durationNode: ChatInstantVideoMessageDurationNode - if let current = strongSelf.durationNode { - durationNode = current - } else { - durationNode = ChatInstantVideoMessageDurationNode(textColor: theme.chat.serviceMessage.serviceMessagePrimaryTextColor, fillColor: theme.chat.serviceMessage.serviceMessageFillColor) - strongSelf.durationNode = durationNode - strongSelf.addSubnode(durationNode) - } - durationNode.defaultDuration = telegramFile.duration.flatMap(Double.init) - - if let videoNode = strongSelf.videoNode { - videoNode.layer.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, delay: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.removeFromSupernode() - }) - } - let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: item.account.telegramApplicationContext.mediaManager.audioSession, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(diameter: 214.0, backgroundImage: instantVideoBackgroundImage, tapped: { - if let strongSelf = self { - if let item = strongSelf.item { - if strongSelf.infoBackgroundNode.alpha.isZero { - item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) - } else { - let _ = item.controllerInteraction.openMessage(item.message) - } - } - } - }), content: NativeVideoContent(id: .message(item.message.id, item.message.stableId, telegramFile.fileId), file: telegramFile, streamVideo: false, enableSound: false), priority: .embedded, autoplay: true) - let previousVideoNode = strongSelf.videoNode - strongSelf.videoNode = videoNode - strongSelf.insertSubnode(videoNode, belowSubnode: previousVideoNode ?? strongSelf.dateAndStatusNode) - videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext - } - - if let durationNode = strongSelf.durationNode { - durationNode.frame = CGRect(origin: CGPoint(x: videoFrame.midX - 56.0, y: videoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) - durationNode.isSeen = !notConsumed - } - - if let videoNode = strongSelf.videoNode { - videoNode.frame = videoFrame - videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate) + strongSelf.interactiveVideoNode.frame = videoFrame + let videoLayoutData: ChatMessageInstantVideoItemLayoutData + if incoming { + videoLayoutData = .constrained(left: 0.0, right: max(0.0, availableContentWidth - videoFrame.width)) + } else { + videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0) } + videoApply(videoLayoutData, transition) if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { if strongSelf.replyBackgroundNode == nil { @@ -515,7 +209,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode strongSelf.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: imageSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - replyInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)), y: videoLayout.contentSize.height - replyInfoSize.height - 8.0), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - 2.0), size: CGSize(width: replyInfoFrame.size.width + 8.0, height: replyInfoFrame.size.height + 5.0)) } else if let replyInfoNode = strongSelf.replyInfoNode { @@ -559,8 +253,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { switch gesture { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.message.author { - item.controllerInteraction.openPeer(author.id, .info, item.message) + if let item = self.item, let author = item.content.firstMessage.author { + let navigate: ChatControllerInteractionNavigateToPeer + if item.content.firstMessage.id.peerId == item.account.peerId { + navigate = .chat(textInputState: nil, messageId: nil) + } else { + navigate = .info + } + item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, navigate, item.message) } return } @@ -587,24 +287,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(location) { - self.progressPressed() - return - } - - if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { - if self.infoBackgroundNode.alpha.isZero { - item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) - } else { - let _ = item.controllerInteraction.openMessage(item.message) - } - return - } - self.item?.controllerInteraction.clickThroughMessage() case .longTap, .doubleTap: - if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { - item.controllerInteraction.openMessageContextMenu(item.message, self, videoNode.frame) + if let item = self.item, let videoContentNode = self.interactiveVideoNode.videoContentNode(at: self.view.convert(location, to: self.interactiveVideoNode.view)) { + item.controllerInteraction.openMessageContextMenu(item.message, videoContentNode, videoContentNode.bounds) } case .hold: break @@ -679,40 +365,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if !self.bounds.contains(point) { return nil } - if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) { - return self.view - } return super.hitTest(point, with: event) } - private func progressPressed() { - guard let item = self.item, let file = self.telegramFile else { - return - } - if let status = self.status { - switch status { - case let .fetchStatus(fetchStatus): - switch fetchStatus { - case .Fetching: - if item.message.flags.isSending { - let messageId = item.message.id - let _ = item.account.postbox.transaction({ transaction -> Void in - transaction.deleteMessages([messageId]) - }).start() - } else { - self.videoNode?.fetchControl(.cancel) - } - case .Remote: - self.videoNode?.fetchControl(.fetch) - case .Local: - break - } - default: - break - } - } - } - override func updateSelectionState(animated: Bool) { guard let item = self.item else { return diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 59dbcdbd10..83983f08d8 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -83,7 +83,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { case let .fetchStatus(fetchStatus): if let account = self.account, let message = self.message, message.flags.isSending { let _ = account.postbox.transaction({ transaction -> Void in - transaction.deleteMessages([message.id]) + deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: [message.id]) }).start() } else { switch fetchStatus { @@ -152,16 +152,15 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if mediaUpdated { if let _ = largestImageRepresentation(file.previewRepresentations) { - updateImageSignal = chatMessageImageFile(account: account, file: file, thumbnail: true) + updateImageSignal = chatMessageImageFile(account: account, fileReference: .message(message: MessageReference(message), media: file), thumbnail: true) } - let messageId = message.id updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file).start()) } }, cancel: { - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: file) }) } diff --git a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift new file mode 100644 index 0000000000..c2c1529570 --- /dev/null +++ b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -0,0 +1,557 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +struct ChatMessageInstantVideoItemLayoutResult { + let contentSize: CGSize + let overflowLeft: CGFloat + let overflowRight: CGFloat +} + +enum ChatMessageInstantVideoItemLayoutData { + case unconstrained(width: CGFloat) + case constrained(left: CGFloat, right: CGFloat) +} + +private let textFont = Font.regular(11.0) + +enum ChatMessageInteractiveInstantVideoNodeStatusType { + case free + case bubble +} + +class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { + private var videoNode: UniversalVideoNode? + + private var statusNode: RadialStatusNode? + private var playbackStatusNode: InstantVideoRadialStatusNode? + private var videoFrame: CGRect? + + private var item: ChatMessageBubbleContentItem? + var telegramFile: TelegramMediaFile? + + private let fetchDisposable = MetaDisposable() + + private var durationNode: ChatInstantVideoMessageDurationNode? + private let dateAndStatusNode: ChatMessageDateAndStatusNode + + private let infoBackgroundNode: ASImageNode + private let muteIconNode: ASImageNode + + private var status: FileMediaResourceStatus? + private let playbackStatusDisposable = MetaDisposable() + + private var shouldAcquireVideoContext: Bool { + if case .visible = self.visibility { + return true + } else { + return false + } + } + + var visibility: ListViewItemNodeVisibility = .none { + didSet { + if self.visibility != oldValue { + self.videoNode?.canAttachContent = self.shouldAcquireVideoContext + } + } + } + + override init() { + self.infoBackgroundNode = ASImageNode() + self.infoBackgroundNode.isLayerBacked = true + self.infoBackgroundNode.displayWithoutProcessing = true + self.infoBackgroundNode.displaysAsynchronously = false + + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + + self.muteIconNode = ASImageNode() + self.muteIconNode.isLayerBacked = true + self.muteIconNode.displayWithoutProcessing = true + self.muteIconNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.dateAndStatusNode) + self.addSubnode(self.infoBackgroundNode) + self.infoBackgroundNode.addSubnode(self.muteIconNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable.dispose() + self.playbackStatusDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.view.addGestureRecognizer(recognizer) + } + + func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) { + let previousFile = self.telegramFile + + let currentItem = self.item + + let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() + + return { item, width, displaySize, statusDisplayType in + var updatedTheme: PresentationTheme? + + var updatedInfoBackgroundImage: UIImage? + var updatedMuteIconImage: UIImage? + if item.presentationData.theme !== currentItem?.presentationData.theme { + updatedTheme = item.presentationData.theme + updatedInfoBackgroundImage = PresentationResourcesChat.chatInstantMessageInfoBackgroundImage(item.presentationData.theme) + updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme) + } + + let instantVideoBackgroundImage: UIImage? + switch statusDisplayType { + case .free: + instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme) + case .bubble: + instantVideoBackgroundImage = nil + } + + let theme = item.presentationData.theme + let isSecretMedia = item.message.containsSecretMedia + + let imageSize = displaySize + + var updatedFile: TelegramMediaFile? + var updatedMedia = false + for media in item.message.media { + if let file = media as? TelegramMediaFile { + updatedFile = file + if let previousFile = previousFile { + updatedMedia = !previousFile.isEqual(file) + } else if previousFile == nil { + updatedMedia = true + } + } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let file = content.file { + updatedFile = file + if let previousFile = previousFile { + updatedMedia = !previousFile.isEqual(file) + } else if previousFile == nil { + updatedMedia = true + } + } + } + + var notConsumed = false + for attribute in item.message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute { + if !attribute.consumed { + notConsumed = true + } + break + } + } + + var updatedPlaybackStatus: Signal? + if let updatedFile = updatedFile, updatedMedia { + updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) + |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in + if let pendingStatus = pendingStatus { + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.27) + } + return .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)) + } else { + return resourceStatus + } + } + } + + let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: imageSize) + + let arguments = TransformImageArguments(corners: ImageCorners(radius: videoFrame.size.width / 2.0), imageSize: videoFrame.size, boundingSize: videoFrame.size, intrinsicInsets: UIEdgeInsets()) + + let statusType: ChatMessageDateAndStatusType + if item.message.effectivelyIncoming(item.account.peerId) { + switch statusDisplayType { + case .free: + statusType = .FreeIncoming + case .bubble: + statusType = .BubbleIncoming + } + } else { + switch statusDisplayType { + case .free: + if item.message.flags.contains(.Failed) { + statusType = .FreeOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .FreeOutgoing(.Sending) + } else { + statusType = .FreeOutgoing(.Sent(read: item.read)) + } + case .bubble: + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + } + + let edited = false + let sentViaBot = false + let viewCount: Int? = nil + /*for attribute in item.message.attributes { + if let _ = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let _ = attribute as? InlineBotMessageAttribute { + sentViaBot = true + } + } + if let author = item.message.author as? TelegramUser, author.botInfo != nil { + sentViaBot = true + }*/ + + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings, format: .minimal) + + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + + let result = ChatMessageInstantVideoItemLayoutResult(contentSize: imageSize, overflowLeft: 0.0, overflowRight: max(0.0, floor(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width)) + + return (result, { [weak self] layoutData, transition in + if let strongSelf = self { + strongSelf.item = item + strongSelf.videoFrame = videoFrame + + if let updatedInfoBackgroundImage = updatedInfoBackgroundImage { + strongSelf.infoBackgroundNode.image = updatedInfoBackgroundImage + } + + if let updatedMuteIconImage = updatedMuteIconImage { + strongSelf.muteIconNode.image = updatedMuteIconImage + } + + strongSelf.telegramFile = updatedFile + + if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image { + let infoWidth = muteImage.size.width + let infoBackgroundFrame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - infoWidth) / 2.0), y: videoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height)) + transition.updateFrame(node: strongSelf.infoBackgroundNode, frame: infoBackgroundFrame) + let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size) + transition.updateFrame(node: strongSelf.muteIconNode, frame: muteIconFrame) + } + + if let updatedPlaybackStatus = updatedPlaybackStatus { + strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in + if let strongSelf = self, let videoFrame = strongSelf.videoFrame { + strongSelf.status = status + + let displayMute: Bool + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case .Local: + displayMute = true + default: + displayMute = false + } + case .playbackStatus: + displayMute = false + } + if displayMute != (!strongSelf.infoBackgroundNode.alpha.isZero) { + if displayMute { + strongSelf.infoBackgroundNode.alpha = 1.0 + strongSelf.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.infoBackgroundNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15) + } else { + strongSelf.infoBackgroundNode.alpha = 0.0 + strongSelf.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) + } + } + + var progressRequired = false + if case let .fetchStatus(fetchStatus) = status { + if case .Local = fetchStatus { + if let file = updatedFile, file.isVideo { + progressRequired = true + } else if isSecretMedia { + progressRequired = true + } + } else { + progressRequired = true + } + } + + if progressRequired { + if strongSelf.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) + statusNode.isUserInteractionEnabled = false + statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) + strongSelf.statusNode = statusNode + strongSelf.addSubnode(statusNode) + } else if let _ = updatedTheme { + + //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) + } + } else { + if let statusNode = strongSelf.statusNode { + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + strongSelf.statusNode = nil + } + } + + var state: RadialStatusNodeState + let bubbleTheme = theme.chat.bubble + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case let .Fetching(isActive, progress): + var adjustedProgress = progress + if isActive { + adjustedProgress = max(adjustedProgress, 0.027) + } + state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) + case .Local: + state = .none + /*if isSecretMedia && secretProgressIcon != nil { + state = .customIcon(secretProgressIcon!) + } else */ + case .Remote: + state = .download(bubbleTheme.mediaOverlayControlForegroundColor) + } + default: + state = .none + break + } + if let statusNode = strongSelf.statusNode { + if state == .none { + strongSelf.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } + + if case .playbackStatus = status { + let playbackStatusNode: InstantVideoRadialStatusNode + if let current = strongSelf.playbackStatusNode { + playbackStatusNode = current + } else { + playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) + strongSelf.addSubnode(playbackStatusNode) + strongSelf.playbackStatusNode = playbackStatusNode + } + playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5) + if let updatedFile = updatedFile { + let status = messageFileMediaPlaybackStatus(account: item.account, file: updatedFile, message: item.message) + playbackStatusNode.status = status + strongSelf.durationNode?.status = status |> map(Optional.init) + } + } else { + if let playbackStatusNode = strongSelf.playbackStatusNode { + strongSelf.playbackStatusNode = nil + playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackStatusNode] _ in + playbackStatusNode?.removeFromSupernode() + }) + } + + strongSelf.durationNode?.status = .single(nil) + } + } + })) + } + + dateAndStatusApply(false) + switch layoutData { + case let .unconstrained(width): + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 55.0, width - dateAndStatusSize.width - 4.0), y: videoFrame.height - dateAndStatusSize.height), size: dateAndStatusSize) + case let .constrained(_, right): + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 55.0, videoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + } + + if let telegramFile = updatedFile, updatedMedia { + let durationTextColor: UIColor + let durationFillColor: UIColor + switch statusDisplayType { + case .free: + durationTextColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor + durationFillColor = theme.chat.serviceMessage.serviceMessageFillColor + case .bubble: + durationFillColor = .clear + if item.message.effectivelyIncoming(item.account.peerId) { + durationTextColor = theme.chat.bubble.incomingSecondaryTextColor + } else { + durationTextColor = theme.chat.bubble.outgoingSecondaryTextColor + } + } + let durationNode: ChatInstantVideoMessageDurationNode + if let current = strongSelf.durationNode { + durationNode = current + current.updateTheme(textColor: durationTextColor, fillColor: durationFillColor) + } else { + durationNode = ChatInstantVideoMessageDurationNode(textColor: durationTextColor, fillColor: durationFillColor) + strongSelf.durationNode = durationNode + strongSelf.addSubnode(durationNode) + } + durationNode.defaultDuration = telegramFile.duration.flatMap(Double.init) + + if let videoNode = strongSelf.videoNode { + videoNode.layer.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, delay: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } + let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: item.account.telegramApplicationContext.mediaManager.audioSession, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(diameter: displaySize.width + 2.0, backgroundImage: instantVideoBackgroundImage, tapped: { + if let strongSelf = self { + if let item = strongSelf.item { + if strongSelf.infoBackgroundNode.alpha.isZero { + item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) + } else { + let _ = item.controllerInteraction.openMessage(item.message) + } + } + } + }), content: NativeVideoContent(id: .message(item.message.id, item.message.stableId, telegramFile.fileId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), streamVideo: false, enableSound: false), priority: .embedded, autoplay: true) + let previousVideoNode = strongSelf.videoNode + strongSelf.videoNode = videoNode + strongSelf.insertSubnode(videoNode, belowSubnode: previousVideoNode ?? strongSelf.dateAndStatusNode) + videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext + } + + if let durationNode = strongSelf.durationNode { + durationNode.frame = CGRect(origin: CGPoint(x: videoFrame.midX - 56.0, y: videoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) + durationNode.isSeen = !notConsumed + } + + if let videoNode = strongSelf.videoNode { + videoNode.frame = videoFrame + videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate) + } + } + }) + } + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(location) { + self.progressPressed() + return + } + + if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { + if self.infoBackgroundNode.alpha.isZero { + item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) + } else { + let _ = item.controllerInteraction.openMessage(item.message) + } + return + } + + self.item?.controllerInteraction.clickThroughMessage() + case .longTap, .doubleTap: + if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { + item.controllerInteraction.openMessageContextMenu(item.message, self, videoNode.frame) + } + case .hold: + break + } + } + default: + break + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) { + return self.view + } + return super.hitTest(point, with: event) + } + + private func progressPressed() { + guard let item = self.item, let _ = self.telegramFile else { + return + } + if let status = self.status { + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case .Fetching: + if item.message.flags.isSending { + let messageId = item.message.id + let _ = item.account.postbox.transaction({ transaction -> Void in + deleteMessages(transaction: transaction, mediaBox: item.account.postbox.mediaBox, ids: [messageId]) + }).start() + } else { + self.videoNode?.fetchControl(.cancel) + } + case .Remote: + self.videoNode?.fetchControl(.fetch) + case .Local: + break + } + default: + break + } + } + } + + func videoContentNode(at point: CGPoint) -> ASDisplayNode? { + if let videoFrame = self.videoFrame { + if videoFrame.contains(point) { + return self.videoNode + } + } + return nil + } + + static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) { + let makeLayout = node?.asyncLayout() + return { item, width, displaySize, statusType in + var createdNode: ChatMessageInteractiveInstantVideoNode? + let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) + if let makeLayout = makeLayout { + sizeAndApplyLayout = makeLayout(item, width, displaySize, statusType) + } else { + let node = ChatMessageInteractiveInstantVideoNode() + sizeAndApplyLayout = node.asyncLayout()(item, width, displaySize, statusType) + createdNode = node + } + return (sizeAndApplyLayout.0, { [weak node] layoutData, transition in + sizeAndApplyLayout.1(layoutData, transition) + if let createdNode = createdNode { + return createdNode + } else { + return node! + } + }) + } + } +} + diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 372625fd01..ea55b37b65 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -80,7 +80,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { case .Fetching: if let account = self.account, let (messageId, flags) = self.messageIdAndFlags, flags.isSending { let _ = account.postbox.transaction({ transaction -> Void in - transaction.deleteMessages([messageId]) + deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: [messageId]) }).start() } if let cancel = self.fetchControls.with({ return $0?.cancel }) { @@ -161,6 +161,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) if file.isAnimated { unboundSize = unboundSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) + } else if file.isSticker { + unboundSize = unboundSize.aspectFilled(CGSize(width: 162.0, height: 162.0)) } isInlinePlayableVideo = file.isVideo && file.isAnimated } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { @@ -251,17 +253,17 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { replaceVideoNode = true } if isSecretMedia { - updateImageSignal = chatSecretPhoto(account: account, photo: image) + updateImageSignal = chatSecretPhoto(account: account, photoReference: .message(message: MessageReference(message), media: image)) } else { - updateImageSignal = chatMessagePhoto(postbox: account.postbox, photo: image) + updateImageSignal = chatMessagePhoto(postbox: account.postbox, photoReference: .message(message: MessageReference(message), media: image)) } updatedFetchControls = FetchControls(fetch: { if let strongSelf = self { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: .message(message: MessageReference(message), media: image)).start()) } }, cancel: { - chatMessagePhotoCancelInteractiveFetch(account: account, photo: image) + chatMessagePhotoCancelInteractiveFetch(account: account, photoReference: .message(message: MessageReference(message), media: image)) }) } else if let image = media as? TelegramMediaWebFile { if hasCurrentVideoNode { @@ -278,9 +280,13 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { }) } else if let file = media as? TelegramMediaFile { if isSecretMedia { - updateImageSignal = chatSecretMessageVideo(account: account, video: file) + updateImageSignal = chatSecretMessageVideo(account: account, videoReference: .message(message: MessageReference(message), media: file)) } else { - updateImageSignal = chatMessageVideo(postbox: account.postbox, video: file) + if file.isSticker { + updateImageSignal = chatMessageSticker(account: account, file: file, small: false) + } else { + updateImageSignal = chatMessageVideo(postbox: account.postbox, videoReference: .message(message: MessageReference(message), media: file)) + } } if isInlinePlayableVideo { @@ -295,20 +301,19 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - let messageId = message.id updatedFetchControls = FetchControls(fetch: { if let strongSelf = self { - if file.isAnimated { - strongSelf.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + if file.isAnimated { + strongSelf.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) } else { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file).start()) } } }, cancel: { if file.isAnimated { account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } else { - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: file) } }) } @@ -317,20 +322,20 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if statusUpdated { if let image = media as? TelegramMediaImage { if message.flags.isSending { - updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(message.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - var progress = pendingStatus.progress - if pendingStatus.isRunning { - progress = max(progress, 0.027) - } - return .Fetching(isActive: pendingStatus.isRunning, progress: progress) - } else { - return resourceStatus + updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photoReference: .message(message: MessageReference(message), media: image)), account.pendingMessageManager.pendingMessageStatus(message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus { + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.027) } + return .Fetching(isActive: pendingStatus.isRunning, progress: progress) + } else { + return resourceStatus + } } } else { - updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) + updatedStatusSignal = chatMessagePhotoStatus(account: account, photoReference: .message(message: MessageReference(message), media: image)) } } else if let file = media as? TelegramMediaFile { updatedStatusSignal = combineLatest(messageMediaFileStatus(account: account, messageId: message.id, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) @@ -378,7 +383,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } if replaceVideoNode, let updatedVideoFile = updateVideoFile { - let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0, nativeSize: nativeSize), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), file: updatedVideoFile, enableSound: false, fetchAutomatically: false), priority: .embedded) + let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0, nativeSize: nativeSize), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), enableSound: false, fetchAutomatically: false), priority: .embedded) videoNode.isUserInteractionEnabled = false strongSelf.videoNode = videoNode @@ -468,7 +473,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) } if case .constrained = sizeCalculation { - if let file = media as? TelegramMediaFile, !file.isAnimated { + if let file = media as? TelegramMediaFile, (!file.isAnimated || message.flags.contains(.Unsent)) { if let size = file.size { badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: "\(dataSizeString(Int(Float(size) * progress))) / \(dataSizeString(size))") } else if let _ = file.duration { @@ -537,11 +542,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if automaticDownload { if let image = media as? TelegramMediaImage { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: .message(message: MessageReference(message), media: image)).start()) } else if let image = media as? TelegramMediaWebFile { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: account, image: image).start()) } else if let file = media as? TelegramMediaFile { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: message.id, file: file).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file).start()) } } } diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index cc1cfdaeba..2ccebb977b 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -54,7 +54,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index ee9f1cbbc3..ba9a3624f7 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -65,7 +65,7 @@ struct ChatMessageItemLayoutConstants { self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 40.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0)) self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.5, left: 1.5, bottom: 1.5, right: 1.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 64.0, height: 64.0)) + self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.5, left: 1.5, bottom: 1.5, right: 1.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 74.0, height: 74.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) self.instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) } diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index 1f7d4969a4..619cb0ecef 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -87,17 +87,18 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let imageSize: CGSize if let selectedMedia = selectedMedia { - if activeLiveBroadcastingTimeout != nil { + if activeLiveBroadcastingTimeout != nil || selectedMedia.venue != nil { let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width) imageSize = CGSize(width: fitWidth, height: floor(fitWidth * 0.5)) - textString = NSAttributedString(string: " ", font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) - } else if let venue = selectedMedia.venue { - imageSize = CGSize(width: 75.0, height: 75.0) - titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingPrimaryTextColor : item.presentationData.theme.chat.bubble.outgoingPrimaryTextColor) - if let address = venue.address, !address.isEmpty { - textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) + if let venue = selectedMedia.venue { + titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingPrimaryTextColor : item.presentationData.theme.chat.bubble.outgoingPrimaryTextColor) + if let address = venue.address, !address.isEmpty { + textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) + } + } else { + textString = NSAttributedString(string: " ", font: textFont, textColor: item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) } } else { let fitWidth: CGFloat = min(constrainedSize.width, layoutConstants.image.maxDimensions.width) @@ -126,10 +127,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } let maximumWidth: CGFloat - if activeLiveBroadcastingTimeout != nil { + if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil { maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right - } else if selectedMedia?.venue != nil { - maximumWidth = CGFloat.greatestFiniteMagnitude } else { maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right } @@ -150,7 +149,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let imageCorners: ImageCorners let maxTextWidth: CGFloat - if activeLiveBroadcastingTimeout != nil { + if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil { var relativePosition = position if case let .linear(top, _) = position { relativePosition = .linear(top: top, bottom: ChatMessageBubbleRelativePosition.Neighbour) @@ -161,11 +160,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } else { maxTextWidth = constrainedSize.width - imageSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.right - if let _ = selectedMedia?.venue { - imageCorners = ImageCorners(radius: 14.0) - } else { - imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - } + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -237,11 +232,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } let contentWidth: CGFloat - if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil { + if let selectedMedia = selectedMedia, selectedMedia.liveBroadcastingTimeout != nil || selectedMedia.venue != nil { contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right - } else if selectedMedia?.venue != nil { - contentWidth = imageSize.width + max(statusSize.width, max(titleLayout.size.width, textLayout.size.width)) + layoutConstants.text.bubbleInsets.right + 8.0 - } else { contentWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right } @@ -258,22 +250,16 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let imageFrame: CGRect - if activeLiveBroadcastingTimeout != nil { + if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil { layoutSize = CGSize(width: imageLayoutSize.width + bubbleInsets.left, height: imageLayoutSize.height + 1.0 + titleLayout.size.height + 1.0 + textLayout.size.height + 10.0) imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top) statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize) } else { - if selectedMedia?.venue != nil { - layoutSize = CGSize(width: contentWidth, height: imageLayoutSize.height + 10.0) - statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize) - imageFrame = baseImageFrame.offsetBy(dx: 5.0, dy: 5.0) - } else { - layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) - statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top) - } + layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) + statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + imageFrame = baseImageFrame.offsetBy(dx: bubbleInsets.left, dy: bubbleInsets.top) } let imageApply = makeImageLayout(arguments) @@ -300,6 +286,11 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: textLayout.size) transition.updateAlpha(node: strongSelf.titleNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0) transition.updateAlpha(node: strongSelf.textNode, alpha: activeLiveBroadcastingTimeout != nil ? 1.0 : 0.0) + } else if selectedMedia?.venue != nil { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + 7.0, y: imageFrame.maxY + 6.0 + titleLayout.size.height), size: textLayout.size) + transition.updateAlpha(node: strongSelf.titleNode, alpha: 1.0) + transition.updateAlpha(node: strongSelf.textNode, alpha: 1.0) } else { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size) @@ -349,7 +340,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.liveTimerNode?.frame = CGRect(origin: CGPoint(x: imageFrame.maxX - 10.0 - timerSize.width, y: imageFrame.maxY + 11.0), size: timerSize) let timerForegroundColor: UIColor = item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingAccentControlColor : item.presentationData.theme.chat.bubble.outgoingAccentControlColor - let timerTextColor: UIColor = item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.chat.bubble.outgoingAccentTextColor + let timerTextColor: UIColor = item.message.effectivelyIncoming(item.account.peerId) ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) if strongSelf.liveTextNode == nil { @@ -461,7 +452,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message) } } } diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index 8aa267f283..eb0956473b 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -149,12 +149,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let updatedMedia = updatedMedia, imageDimensions != nil { if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = mediaGridMessagePhoto(account: item.account, photo: image) + updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .message(message: MessageReference(item.message), media: image)) } else if let file = updatedMedia as? TelegramMediaFile { if file.isSticker { updateImageSignal = chatMessageSticker(account: item.account, file: file, small: true, fetched: true) } else if file.isVideo { - updateImageSignal = mediaGridMessageVideo(postbox: item.account.postbox, video: file) + updateImageSignal = mediaGridMessageVideo(postbox: item.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file)) } } } diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index 2eb03d8a12..d31d398f51 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -26,7 +26,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { private var textNode: TextNode? private var imageNode: TransformImageNode? private var overlayIconNode: ASImageNode? - private var previousMedia: Media? + private var previousMediaReference: AnyMediaReference? override init() { self.contentNode = ASDisplayNode() @@ -51,7 +51,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) - let previousMedia = maybeNode?.previousMedia + let previousMediaReference = maybeNode?.previousMediaReference return { theme, strings, account, type, message, constrainedSize in let titleString = message.author?.displayTitle ?? "" @@ -80,19 +80,19 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var overlayIcon: UIImage? - var updatedMedia: Media? + var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? var hasRoundImage = false if !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { - updatedMedia = image + updatedMediaReference = .message(message: MessageReference(message), media: image) if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions } break } else if let file = media as? TelegramMediaFile, file.isVideo { - updatedMedia = file + updatedMediaReference = .message(message: MessageReference(message), media: file) if let dimensions = file.dimensions { imageDimensions = dimensions @@ -125,22 +125,21 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (previousMedia != nil) { + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(previousMediaReference.media) + } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { mediaUpdated = true } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - if let updatedMedia = updatedMedia, mediaUpdated && imageDimensions != nil { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) - } else if let file = updatedMedia as? TelegramMediaFile { - if file.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) - } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + updateImageSignal = chatMessagePhotoThumbnail(account: account, photoReference: imageReference) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, fileReference: fileReference) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: account, fileReference: fileReference, representation: iconImageRepresentation) } } } @@ -162,7 +161,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node = ChatMessageReplyInfoNode() } - node.previousMedia = updatedMedia + node.previousMediaReference = updatedMediaReference let titleNode = titleApply() let textNode = textApply() diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 71b5443217..8ab22eb6c4 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -115,13 +115,16 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) + + let panelHeight: CGFloat = 45.0 + if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } if let actions = self.actions { self.deleteButton.isEnabled = false self.reportButton.isEnabled = false - self.forwardButton.isEnabled = true + self.forwardButton.isEnabled = actions.options.contains(.forward) self.shareButton.isEnabled = false self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty @@ -148,9 +151,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } if self.reportButton.isHidden { - self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) - self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) - self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) + self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) } else if !self.deleteButton.isHidden { let buttons: [UIButton] = [ self.deleteButton, @@ -158,7 +161,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.shareButton, self.forwardButton ] - let buttonSize = CGSize(width: 57.0, height: 47.0) + let buttonSize = CGSize(width: 57.0, height: panelHeight) let availableWidth = width - leftInset - rightInset let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1)) @@ -173,13 +176,13 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { offset += buttonSize.width + spacing } } else { - self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) - self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: panelHeight)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) self.reportButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) - self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) } - return 45.0 + return panelHeight } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index a5ae9e9ab4..5c4097cb28 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -13,6 +13,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var swipeToReplyFeedback: HapticFeedback? private var selectionNode: ChatMessageSelectionNode? + private var shareButtonNode: HighlightableButtonNode? var telegramFile: TelegramMediaFile? @@ -49,7 +50,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in + recognizer.tapActionAtPoint = { [weak self] point in + if let strongSelf = self { + if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { + return .fail + } + } return .waitForSingleTap } self.view.addGestureRecognizer(recognizer) @@ -77,7 +83,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let signal = chatMessageSticker(account: item.account, file: telegramFile, small: false) self.imageNode.setSignal(signal) - self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: telegramFile).start()) + self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) } break @@ -94,6 +100,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentReplyBackgroundNode = self.replyBackgroundNode + let currentShareButtonNode = self.shareButtonNode + let currentItem = self.item return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in let incoming = item.message.effectivelyIncoming(item.account.peerId) @@ -111,15 +119,19 @@ class ChatMessageStickerItemNode: ChatMessageItemView { switch item.chatLocation { case let .peer(peerId): - if peerId.isGroupOrChannel && item.message.author != nil { - var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true - } - - if !isBroadcastChannel { - hasAvatar = true + if peerId != item.account.peerId { + if peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } } + } else if incoming { + hasAvatar = true } case .group: hasAvatar = true @@ -131,6 +143,45 @@ class ChatMessageStickerItemNode: ChatMessageItemView { avatarInset = 0.0 } + var needShareButton = false + if item.message.id.peerId == item.account.peerId { + for attribute in item.content.firstMessage.attributes { + if let _ = attribute as? SourceReferenceMessageAttribute { + needShareButton = true + break + } + } + } else if item.message.effectivelyIncoming(item.account.peerId) { + if let peer = item.message.peers[item.message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + needShareButton = true + } + } + } + if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo { + needShareButton = true + } + if !needShareButton { + loop: for media in item.message.media { + if media is TelegramMediaGame || media is TelegramMediaInvoice { + needShareButton = true + break loop + } else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content { + needShareButton = true + break loop + } + } + } else { + loop: for media in item.message.media { + if media is TelegramMediaAction { + needShareButton = false + break loop + } + } + } + } + var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) if dateHeaderAtBottom { layoutInsets.top += layoutConstants.timestampHeaderHeight @@ -157,23 +208,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - var edited = false - var sentViaBot = false - var viewCount: Int? - for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute { - edited = true - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let _ = attribute as? InlineBotMessageAttribute { - sentViaBot = true - } - } - if let author = item.message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true - } + let edited = false + let sentViaBot = false + let viewCount: Int? = nil - let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) + let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings, format: .minimal) let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) @@ -195,6 +234,32 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + var updatedShareButtonBackground: UIImage? + + var updatedShareButtonNode: HighlightableButtonNode? + if needShareButton { + if currentShareButtonNode != nil { + updatedShareButtonNode = currentShareButtonNode + if item.presentationData.theme !== currentItem?.presentationData.theme { + if item.message.id.peerId == item.account.peerId { + updatedShareButtonBackground = PresentationResourcesChat.chatBubbleNavigateButtonImage(item.presentationData.theme) + } else { + updatedShareButtonBackground = PresentationResourcesChat.chatBubbleShareButtonImage(item.presentationData.theme) + } + } + } else { + let buttonNode = HighlightableButtonNode() + let buttonIcon: UIImage? + if item.message.id.peerId == item.account.peerId { + buttonIcon = PresentationResourcesChat.chatBubbleNavigateButtonImage(item.presentationData.theme) + } else { + buttonIcon = PresentationResourcesChat.chatBubbleShareButtonImage(item.presentationData.theme) + } + buttonNode.setBackgroundImage(buttonIcon, for: [.normal]) + updatedShareButtonNode = buttonNode + } + } + let contentHeight = max(imageSize.height, layoutConstants.image.minDimensions.height) return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: layoutInsets), { [weak self] animation in @@ -205,6 +270,27 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.progressNode?.position = strongSelf.imageNode.position imageApply() + if let updatedShareButtonNode = updatedShareButtonNode { + if updatedShareButtonNode !== strongSelf.shareButtonNode { + if let shareButtonNode = strongSelf.shareButtonNode { + shareButtonNode.removeFromSupernode() + } + strongSelf.shareButtonNode = updatedShareButtonNode + strongSelf.addSubnode(updatedShareButtonNode) + updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) + } + if let updatedShareButtonBackground = updatedShareButtonBackground { + strongSelf.shareButtonNode?.setBackgroundImage(updatedShareButtonBackground, for: [.normal]) + } + } else if let shareButtonNode = strongSelf.shareButtonNode { + shareButtonNode.removeFromSupernode() + strongSelf.shareButtonNode = nil + } + + if let shareButtonNode = strongSelf.shareButtonNode { + shareButtonNode.frame = CGRect(origin: CGPoint(x: updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) + } + dateAndStatusApply(false) strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) @@ -244,23 +330,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView { switch gesture { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { - if let item = self.item, let author = item.message.author { - item.controllerInteraction.openPeer(author.id, .info, item.message) + if let item = self.item, let author = item.content.firstMessage.author { + let navigate: ChatControllerInteractionNavigateToPeer + if item.content.firstMessage.id.peerId == item.account.peerId { + navigate = .chat(textInputState: nil, messageId: nil) + } else { + navigate = .info + } + item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, navigate, item.message) } return } - /*if let nameNode = self.nameNode, nameNode.frame.contains(location) { - if let item = self.item { - for attribute in item.message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName { - self.controllerInteraction?.updateInputState { textInputState in - return ChatTextInputState(inputText: "@" + addressName + " ") - } - return - } - } - } - } else */ if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { if let item = self.item { @@ -272,20 +352,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } - - /*if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { - if let item = self.item, let forwardInfo = item.message.forwardInfo { - if let sourceMessageId = forwardInfo.sourceMessageId { - self.controllerInteraction?.navigateToMessage(item.message.id, sourceMessageId) - } else { - self.controllerInteraction?.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil)) - } - return - } - }*/ if let item = self.item, self.imageNode.frame.contains(location) { - item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message) return } @@ -303,6 +372,21 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + @objc func shareButtonPressed() { + if let item = self.item { + if item.content.firstMessage.id.peerId == item.account.peerId { + for attribute in item.content.firstMessage.attributes { + if let attribute = attribute as? SourceReferenceMessageAttribute { + item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) + break + } + } + } else { + item.controllerInteraction.openMessageShareMenu(item.message.id) + } + } + } + @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { switch recognizer.state { case .began: @@ -364,6 +448,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { + return shareButtonNode.view + } + return super.hitTest(point, with: event) } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index eae9cbf29a..8f3ca5e8a9 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -227,7 +227,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.controllerInteraction, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 40fe73b8c2..bf3b26dcac 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -42,11 +42,12 @@ final class ChatPanelInterfaceInteraction { let beginMessageSelection: ([MessageId]) -> Void let deleteSelectedMessages: () -> Void let reportSelectedMessages: () -> Void + let reportMessages: ([Message]) -> Void let deleteMessages: ([Message]) -> Void let forwardSelectedMessages: () -> Void let forwardMessages: ([Message]) -> Void let shareSelectedMessages: () -> Void - let updateTextInputState: (@escaping (ChatTextInputState) -> ChatTextInputState) -> Void + let updateTextInputStateAndMode: (@escaping (ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void let editMessage: () -> Void let beginMessageSearch: (ChatSearchDomain, String) -> Void @@ -71,7 +72,7 @@ final class ChatPanelInterfaceInteraction { let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject) -> Void let switchMediaRecordingMode: () -> Void let setupMessageAutoremoveTimeout: () -> Void - let sendSticker: (TelegramMediaFile) -> Void + let sendSticker: (FileMediaReference) -> Void let unblockPeer: () -> Void let pinMessage: (MessageId) -> Void let unpinMessage: () -> Void @@ -87,17 +88,18 @@ final class ChatPanelInterfaceInteraction { let toggleSilentPost: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message]) -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.reportSelectedMessages = reportSelectedMessages + self.reportMessages = reportMessages self.deleteMessages = deleteMessages self.forwardSelectedMessages = forwardSelectedMessages self.forwardMessages = forwardMessages self.shareSelectedMessages = shareSelectedMessages - self.updateTextInputState = updateTextInputState + self.updateTextInputStateAndMode = updateTextInputStateAndMode self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId self.editMessage = editMessage self.beginMessageSearch = beginMessageSearch diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index 08d3bf819a..34cc217947 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -18,7 +18,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private var currentLayout: (CGFloat, CGFloat, CGFloat)? private var currentMessage: Message? - private var previousMedia: Media? + private var previousMediaReference: AnyMediaReference? private let queue = Queue() @@ -117,10 +117,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } } - let leftInset: CGFloat = 10.0 + leftInset + let contentLeftInset: CGFloat = 10.0 + leftInset let rightInset: CGFloat = 18.0 + rightInset - transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: CGSize(width: 2.0, height: panelHeight - 14.0))) + transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: contentLeftInset, y: 7.0), size: CGSize(width: 2.0, height: panelHeight - 14.0))) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize)) @@ -144,7 +144,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let makeTextLayout = TextNode.asyncLayout(self.textNode) let imageNodeLayout = self.imageNode.asyncLayout() - let previousMedia = self.previousMedia + let previousMediaReference = self.previousMediaReference let account = self.account let targetQueue: Queue @@ -155,22 +155,22 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } targetQueue.async { [weak self] in - let leftInset: CGFloat = 10.0 + let contentLeftInset: CGFloat = leftInset + 10.0 var textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 18.0 + rightInset let textRightInset: CGFloat = 0.0 - var updatedMedia: Media? + var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? for media in message.media { if let image = media as? TelegramMediaImage { - updatedMedia = image + updatedMediaReference = .message(message: MessageReference(message), media: image) if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions } break } else if let file = media as? TelegramMediaFile { - updatedMedia = file + updatedMediaReference = .message(message: MessageReference(message), media: file) if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { imageDimensions = representation.dimensions } @@ -187,23 +187,22 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (previousMedia != nil) { + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(previousMediaReference.media) + } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { mediaUpdated = true } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { - if let updatedMedia = updatedMedia, imageDimensions != nil { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) - } else if let file = updatedMedia as? TelegramMediaFile { - if file.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) - } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + updateImageSignal = chatMessagePhotoThumbnail(account: account, photoReference: imageReference) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, fileReference: fileReference) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: account, fileReference: fileReference, representation: iconImageRepresentation) } } } else { @@ -211,22 +210,22 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId).0, font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId).0, font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { let _ = titleApply() let _ = textApply() - strongSelf.previousMedia = updatedMedia + strongSelf.previousMediaReference = updatedMediaReference - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 5.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 5.0), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 23.0), size: textLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 23.0), size: textLayout.size) - strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0)) if let applyImage = applyImage { applyImage() diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift index 6ec2228ae2..c29c8d4c1a 100644 --- a/TelegramUI/ChatRecentActionsController.swift +++ b/TelegramUI/ChatRecentActionsController.swift @@ -41,11 +41,12 @@ final class ChatRecentActionsController: ViewController { }, beginMessageSelection: { _ in }, deleteSelectedMessages: { }, reportSelectedMessages: { + }, reportMessages: { _ in }, deleteMessages: { _ in }, forwardSelectedMessages: { }, forwardMessages: { _ in }, shareSelectedMessages: { - }, updateTextInputState: { _ in + }, updateTextInputStateAndMode: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { }, beginMessageSearch: { _, _ in diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index cb9e1a7b93..1cd38a16f2 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -125,6 +125,24 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { + guard let state = strongSelf.listNode.opaqueTransactionState as? ChatRecentActionsListOpaqueState else { + return false + } + for entry in state.entries { + if entry.entry.stableId == message.stableId { + switch entry.entry.event.action { + case let .changeStickerPack(_, new): + if let new = new { + strongSelf.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: new, parentNavigationController: strongSelf.getNavigationController()), nil) + return true + } + default: + break + } + + break + } + } return openChatMessage(account: account, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: { //self?.chatDisplayNode.dismissInput() }, present: { c, a in @@ -599,9 +617,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private func openMessageContextMenu(message: Message, node: ASDisplayNode, frame: CGRect) { var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: { - UIPasteboard.general.string = message.text - })) + if !message.text.isEmpty { + actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = message.text + })) + } if let author = message.author, let adminsState = self.adminsState { var canBan = author.id != self.account.peerId diff --git a/TelegramUI/ChatRecentActionsHistoryTransition.swift b/TelegramUI/ChatRecentActionsHistoryTransition.swift index fdac6afc6a..6728547e53 100644 --- a/TelegramUI/ChatRecentActionsHistoryTransition.swift +++ b/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -603,18 +603,28 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if case let .member(_, _, newAdminRights, _) = new.participant { let prevFlags = prevAdminRights?.rights.flags ?? [] let newFlags = newAdminRights?.rights.flags ?? [] - - let order: [(TelegramChannelAdminRightsFlags, String)] = [ - (.canChangeInfo, self.presentationData.strings.Channel_AdminLog_CanChangeInfo), - (.canPostMessages, self.presentationData.strings.Channel_AdminLog_CanSendMessages), - (.canDeleteMessages, self.presentationData.strings.Channel_AdminLog_CanDeleteMessages), - (.canBanUsers, self.presentationData.strings.Channel_AdminLog_CanBanUsers), - (.canEditMessages, self.presentationData.strings.Channel_AdminLog_CanEditMessages), - (.canInviteUsers, self.presentationData.strings.Channel_AdminLog_CanChangeInviteLink), - (.canChangeInviteLink, self.presentationData.strings.Channel_AdminLog_CanInviteUsers), - (.canPinMessages, self.presentationData.strings.Channel_AdminLog_CanPinMessages), - (.canAddAdmins, self.presentationData.strings.Channel_AdminLog_CanAddAdmins) - ] + + let order: [(TelegramChannelAdminRightsFlags, String)] + + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + order = [ + (.canChangeInfo, self.presentationData.strings.Channel_AdminLog_CanChangeInfo), + (.canPostMessages, self.presentationData.strings.Channel_AdminLog_CanSendMessages), + (.canDeleteMessages, self.presentationData.strings.Channel_AdminLog_CanDeleteMessages), + (.canEditMessages, self.presentationData.strings.Channel_AdminLog_CanEditMessages), + (.canInviteUsers, self.presentationData.strings.Channel_AdminLog_CanInviteUsers), + (.canAddAdmins, self.presentationData.strings.Channel_AdminLog_CanAddAdmins) + ] + } else { + order = [ + (.canChangeInfo, self.presentationData.strings.Channel_AdminLog_CanChangeInfo), + (.canDeleteMessages, self.presentationData.strings.Channel_AdminLog_CanDeleteMessages), + (.canBanUsers, self.presentationData.strings.Channel_AdminLog_CanBanUsers), + (.canPinMessages, self.presentationData.strings.Channel_AdminLog_CanPinMessages), + (.canChangeInviteLink, self.presentationData.strings.Channel_AdminLog_CanInviteUsers), + (.canAddAdmins, self.presentationData.strings.Channel_AdminLog_CanAddAdmins) + ] + } for (flag, string) in order { if prevFlags.contains(flag) != newFlags.contains(flag) { diff --git a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift index ab96274223..462473b38b 100644 --- a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift +++ b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -110,7 +110,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.mediaPlayer?.pause() } if let account = self.account { - let mediaPlayer = MediaPlayer(audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, postbox: account.postbox, resource: recordedMediaPreview.resource, streamable: false, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) + let mediaPlayer = MediaPlayer(audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, postbox: account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: false, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) self.mediaPlayer = mediaPlayer self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration) self.durationLabel.status = mediaPlayer.status @@ -131,9 +131,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 - transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset, y: -1.0), size: CGSize(width: 48.0, height: 47.0))) + transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset, y: -1.0), size: CGSize(width: 48.0, height: panelHeight))) transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: -UIScreenPixel), size: CGSize(width: 44.0, height: panelHeight))) transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) transition.updateFrame(node: self.pauseButton, frame: CGRect(origin: CGPoint(x: leftInset + 50.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) diff --git a/TelegramUI/ChatRestrictedInputPanelNode.swift b/TelegramUI/ChatRestrictedInputPanelNode.swift index 5f13b650ed..278e37e4eb 100644 --- a/TelegramUI/ChatRestrictedInputPanelNode.swift +++ b/TelegramUI/ChatRestrictedInputPanelNode.swift @@ -33,7 +33,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } } - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0), y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 509c095430..3bc59ad4e6 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -104,7 +104,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { } } - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 transition.updateFrame(node: self.downButton, frame: CGRect(origin: CGPoint(x: leftInset + 12.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) transition.updateFrame(node: self.upButton, frame: CGRect(origin: CGPoint(x: leftInset + 12.0 + 43.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) diff --git a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift index 057261a6d1..33656661d6 100644 --- a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift +++ b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift @@ -122,7 +122,7 @@ private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPi } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 157.0) + return CGSize(width: constrainedSize.width, height: 180.0) } func numberOfComponents(in pickerView: UIPickerView) -> Int { @@ -133,6 +133,10 @@ private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPi return timeoutValues.count } + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + return 40.0 + } + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { if timeoutValues[row] == 0 { return NSAttributedString(string: self.strings.Profile_MessageLifetimeForever, font: Font.medium(15.0), textColor: self.theme.primaryTextColor) diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index c423d1de2a..fbce927f3a 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -500,6 +500,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) let baseWidth = width - leftInset - rightInset + + var wasEditingMedia = false + if let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState { + if case let .media(value) = editMessageState.content { + wasEditingMedia = !value.isEmpty + } + } + + var isMediaEnabled = true + var isEditingMedia = false + if let editMessageState = interfaceState.editMessageState { + if case let .media(value) = editMessageState.content { + isEditingMedia = !value.isEmpty + isMediaEnabled = !value.isEmpty + } else { + isMediaEnabled = false + } + } + transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.5) + self.attachmentButton.isEnabled = isMediaEnabled + if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -539,7 +560,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.theme = interfaceState.theme + if isEditingMedia { + self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: []) + } else { self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) + } self.actionButtons.updateTheme(theme: interfaceState.theme) @@ -559,18 +584,32 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { for (_, button) in self.accessoryItemButtons { button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) } - } else if self.strings !== interfaceState.strings { - self.strings = interfaceState.strings + } else { + if self.strings !== interfaceState.strings { + self.strings = interfaceState.strings + + for (_, button) in self.accessoryItemButtons { + button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) + } + } - for (_, button) in self.accessoryItemButtons { - button.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) + if wasEditingMedia != isEditingMedia { + if isEditingMedia { + self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: []) + } else { + self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) + } } } - if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) { + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting { let placeholder: String if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder + if interfaceState.interfaceState.silentPosting { + placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder + } else { + placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder + } } else { placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } @@ -579,6 +618,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { + self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) let _ = placeholderApply() } @@ -827,18 +873,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if let presentationInterfaceState = self.presentationInterfaceState { - var isMediaEnabled = true - if let editMessageState = presentationInterfaceState.editMessageState { - if case .media(true) = editMessageState.content { - isMediaEnabled = true - } else { - isMediaEnabled = false - } - } - transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.5) - self.attachmentButton.isEnabled = isMediaEnabled - } transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight + audioRecordingItemsVerticalOffset), size: CGSize(width: 40.0, height: minimalHeight))) var composeButtonsOffset: CGFloat = 0.0 @@ -848,7 +882,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputBackgroundWidthOffset = 36.0 } - transition.updateFrame(node: self.actionButtons, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) + transition.updateFrame(node: self.actionButtons, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))) if let presentationInterfaceState = self.presentationInterfaceState { self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState) } @@ -970,7 +1004,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let inputTextState = self.inputTextState - self.interfaceInteraction?.updateTextInputState({ _ in return inputTextState }) + self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) self.updateTextNodeText(animated: true) } } @@ -1149,7 +1183,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { if !dueToEditing && !updatingInputState { let inputTextState = self.inputTextState - self.interfaceInteraction?.updateTextInputState({ _ in return inputTextState }) + self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) } if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { @@ -1169,11 +1203,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return (.text, state.keyboardButtonsMessage?.id) }) if activateGifInput { - self.interfaceInteraction?.updateTextInputState { state in + self.interfaceInteraction?.updateTextInputStateAndMode { state, inputMode in if state.inputText.length == 0 { - return ChatTextInputState(inputText: NSAttributedString(string: "@gif ")) + return (ChatTextInputState(inputText: NSAttributedString(string: "@gif ")), inputMode) } else { - return state + return (state, inputMode) } } } @@ -1213,22 +1247,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func formatAttributesBold(_ sender: Any) { self.inputMenu.back() - self.interfaceInteraction?.updateTextInputState { current in - return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold) + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold), inputMode) } } @objc func formatAttributesItalic(_ sender: Any) { self.inputMenu.back() - self.interfaceInteraction?.updateTextInputState { current in - return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic) + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic), inputMode) } } @objc func formatAttributesMonospace(_ sender: Any) { self.inputMenu.back() - self.interfaceInteraction?.updateTextInputState { current in - return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace) + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace), inputMode) } } @@ -1270,7 +1304,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func searchLayoutClearButtonPressed() { if let interfaceInteraction = self.interfaceInteraction { - interfaceInteraction.updateTextInputState { textInputState in + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var mentionQueryRange: NSRange? inner: for (_, type, queryRange) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.contextRequest] { @@ -1286,9 +1320,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { inputText.replaceCharacters(in: NSRange(location: rangeLower, length: rangeUpper - rangeLower), with: "") - return ChatTextInputState(inputText: inputText) + return (ChatTextInputState(inputText: inputText), inputMode) } else { - return ChatTextInputState(inputText: NSAttributedString(string: "")) + return (ChatTextInputState(inputText: NSAttributedString(string: "")), inputMode) } } } @@ -1351,8 +1385,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return (.inputButtons, nil) }) case .commands: - self.interfaceInteraction?.updateTextInputState { _ in - return ChatTextInputState(inputText: NSAttributedString(string: "/")) + self.interfaceInteraction?.updateTextInputStateAndMode { _, inputMode in + return (ChatTextInputState(inputText: NSAttributedString(string: "/")), .text) } case .silentPost: self.interfaceInteraction?.toggleSilentPost() diff --git a/TelegramUI/ChatToastAlertPanelNode.swift b/TelegramUI/ChatToastAlertPanelNode.swift index ba7c96f09e..54237187d8 100644 --- a/TelegramUI/ChatToastAlertPanelNode.swift +++ b/TelegramUI/ChatToastAlertPanelNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { private let separatorNode: ASDisplayNode - private let titleNode: ASTextNode + private let titleNode: ImmediateTextNode private var textColor: UIColor = .black { didSet { @@ -27,7 +27,7 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.titleNode = ASTextNode() + self.titleNode = ImmediateTextNode() self.titleNode.attributedText = NSAttributedString(string: "", font: Font.regular(14.0), textColor: UIColor.black) self.titleNode.maximumNumberOfLines = 1 @@ -46,7 +46,7 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) - let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - rightInset - 20.0, height: 100.0)) + let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - rightInset - 20.0, height: 100.0)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((panelHeight - titleSize.height) / 2.0)), size: titleSize) return panelHeight diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift index 9e97404365..0853f31558 100644 --- a/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -51,7 +51,6 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.addSubnode(self.button) self.view.addSubview(self.activityIndicator) - self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) } @@ -64,7 +63,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.theme = theme self.strings = strings - self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + self.button.setAttributedTitle(NSAttributedString(string: self.button.attributedTitle(for: [])?.string ?? "", font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) } } @@ -83,18 +82,35 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState + + + let string: NSAttributedString + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + string = NSAttributedString(string: strings.Bot_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor) + } else { + string = NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor) + } + let updated: Bool + if let current = self.button.attributedTitle(for: []) { + updated = !current.isEqual(to: string) + } else { + updated = true + } + if updated { + self.button.setAttributedTitle(string, for: []) + } } let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 80.0, height: 100.0)) - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 45.0 + return panelHeight } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/TelegramUI/CheckDeviceAccess.swift b/TelegramUI/CheckDeviceAccess.swift new file mode 100644 index 0000000000..34bcbe4e93 --- /dev/null +++ b/TelegramUI/CheckDeviceAccess.swift @@ -0,0 +1,158 @@ +import Foundation +import UIKit +import AVFoundation +import Display +import SwiftSignalKit +import Photos +import CoreLocation + +import LegacyComponents + +enum DeviceAccessMicrophoneSubject { + case audio + case video +} + +enum DeviceAccessMediaLibrarySubject { + case send + case save +} + +enum DeviceAccessLocationSubject { + case send + case live + case tracking +} + +enum DeviceAccessSubject { + case camera + case microphone(DeviceAccessMicrophoneSubject) + case mediaLibrary(DeviceAccessMediaLibrarySubject) + case location(DeviceAccessLocationSubject) +} + +private enum AccessType { + case allowed + case denied + case restricted +} + +private let cachedMediaLibraryAccessStatus = Atomic(value: nil) + +func authorizeDeviceAccess(to subject: DeviceAccessSubject, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, openSettings: @escaping () -> Void, _ completion: @escaping (Bool) -> Void) { + switch subject { + case .camera: + let status = PGCamera.cameraAuthorizationStatus() + if status == PGCameraAuthorizationStatusNotDetermined { + completion(true) + } else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied { + let text: String + if status == PGCameraAuthorizationStatusRestricted { + text = presentationData.strings.AccessDenied_CameraRestricted + } else { + text = presentationData.strings.AccessDenied_Camera + } + completion(false) + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } else if status == PGCameraAuthorizationStatusAuthorized { + completion(true) + } else { + assertionFailure() + completion(true) + } + case let .microphone(microphoneSubject): + if AVAudioSession.sharedInstance().recordPermission() == .granted { + completion(true) + } else { + AVAudioSession.sharedInstance().requestRecordPermission({ granted in + if granted { + completion(true) + } else { + completion(false) + let text: String + switch microphoneSubject { + case .audio: + text = presentationData.strings.AccessDenied_VoiceMicrophone + case .video: + text = presentationData.strings.AccessDenied_VideoMicrophone + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + }) + } + case let .mediaLibrary(mediaLibrarySubject): + let continueWithValue: (Bool) -> Void = { value in + Queue.mainQueue().async { + if value { + completion(true) + } else { + completion(false) + let text: String + switch mediaLibrarySubject { + case .send: + text = presentationData.strings.AccessDenied_PhotosAndVideos + case .save: + text = presentationData.strings.AccessDenied_SaveMedia + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + } + } + if let value = cachedMediaLibraryAccessStatus.with({ $0 }) { + continueWithValue(value) + } else { + PHPhotoLibrary.requestAuthorization({ status in + let value: Bool + switch status { + case .restricted, .denied, .notDetermined: + value = false + case .authorized: + value = true + } + let _ = cachedMediaLibraryAccessStatus.swap(value) + continueWithValue(value) + }) + } + case let .location(locationSubject): + let status = CLLocationManager.authorizationStatus() + switch status { + case .authorizedAlways: + completion(true) + case .authorizedWhenInUse: + switch locationSubject { + case .send, .tracking: + completion(true) + case .live: + completion(false) + let text = presentationData.strings.AccessDenied_LocationAlwaysDenied + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + case .denied, .restricted: + completion(false) + let text: String + if status == .denied { + switch locationSubject { + case .send, .live: + text = presentationData.strings.AccessDenied_LocationDenied + case .tracking: + text = presentationData.strings.AccessDenied_LocationTracking + } + } else { + text = presentationData.strings.AccessDenied_LocationDisabled + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + case .notDetermined: + completion(true) + } + } +} diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index b24f6cf252..51d1867e22 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -101,7 +101,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { if sendImmediately { interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) } else { - interfaceInteraction.updateTextInputState { textInputState in + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var commandQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.command] { @@ -119,9 +119,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let selectionPosition = range.lowerBound + (replacementText as NSString).length - return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } - return textInputState + return (textInputState, inputMode) } } } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 5a2b362227..62e2516355 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -468,7 +468,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 - strongSelf.statusNode.alpha = item.enabled ? 1.0 : 0.4 + strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 let _ = statusApply() transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size)) diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index e9ce3d41c8..396951f51d 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -141,7 +141,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = arc4random64() - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), reference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) return .message(text: "", attributes: [], media: file, replyToMessageId: nil, localGroupingKey: nil) } let _ = enqueueMessages(account: arguments.account, peerId: peerId, messages: messages).start() diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift index 4ea7a452eb..8df5f6ea25 100644 --- a/TelegramUI/DeleteChatInputPanelNode.swift +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -42,7 +42,7 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { let buttonSize = self.button.measure(CGSize(width: width - leftInset - rightInset - 10.0, height: 100.0)) - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index 5288e8eb2a..b350a2fbe7 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -22,8 +22,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { private let editingMessageDisposable = MetaDisposable() private var currentMessage: Message? - private var currentEditMedia: Media? - private var previousMedia: Media? + private var currentEditMediaReference: AnyMediaReference? + private var previousMediaReference: AnyMediaReference? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -126,35 +126,35 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { var text = "" if let message = message { var effectiveMessage = message - if let currentEditMedia = self.currentEditMedia { - effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMedia]) + if let currentEditMediaReference = self.currentEditMediaReference { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } (text, _) = descriptionStringForMessage(effectiveMessage, strings: self.strings, accountPeerId: self.account.peerId) } - var updatedMedia: Media? + var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? if let message = message, !message.containsSecretMedia { - var candidateMedia: Media? - if let currentEditMedia = self.currentEditMedia { - candidateMedia = currentEditMedia + var candidateMediaReference: AnyMediaReference? + if let currentEditMedia = self.currentEditMediaReference { + candidateMediaReference = currentEditMedia } else { for media in message.media { if media is TelegramMediaImage || media is TelegramMediaFile { - candidateMedia = media + candidateMediaReference = .message(message: MessageReference(message), media: media) break } } } - if let image = candidateMedia as? TelegramMediaImage { - updatedMedia = image - if let representation = largestRepresentationForPhoto(image) { + if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) { + updatedMediaReference = imageReference.abstract + if let representation = largestRepresentationForPhoto(imageReference.media) { imageDimensions = representation.dimensions } - } else if let file = candidateMedia as? TelegramMediaFile { - updatedMedia = file - if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + } else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) { + updatedMediaReference = fileReference.abstract + if !fileReference.media.isInstantVideo, let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker { imageDimensions = representation.dimensions } } @@ -168,24 +168,23 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = self.previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (self.previousMedia != nil) { + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = self.previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(previousMediaReference.media) + } else if (updatedMediaReference != nil) != (self.previousMediaReference != nil) { mediaUpdated = true } - self.previousMedia = updatedMedia + self.previousMediaReference = updatedMediaReference var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { - if let updatedMedia = updatedMedia, imageDimensions != nil { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: self.account, photo: image) - } else if let file = updatedMedia as? TelegramMediaFile { - if file.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: self.account, file: file) - } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateImageSignal = chatWebpageSnippetPhoto(account: self.account, photo: tmpImage) + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + updateImageSignal = chatMessagePhotoThumbnail(account: self.account, photoReference: imageReference) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: self.account, fileReference: fileReference) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: account, fileReference: fileReference, representation: iconImageRepresentation) } } } else { @@ -196,8 +195,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let isMedia: Bool if let message = message { var effectiveMessage = message - if let currentEditMedia = self.currentEditMedia { - effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMedia]) + if let currentEditMediaReference = self.currentEditMediaReference { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } switch messageContentKind(effectiveMessage, strings: strings, accountPeerId: self.account.peerId) { case .text: @@ -209,7 +208,12 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { isMedia = false } - let canEditMedia = message.flatMap(canEditMessageMedia) ?? false + let canEditMedia: Bool + if let message = message, !messageMediaEditingOptions(message: message).isEmpty { + canEditMedia = true + } else { + canEditMedia = false + } self.titleNode.attributedText = NSAttributedString(string: canEditMedia ? self.strings.Conversation_EditingCaptionPanelTitle : self.strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) @@ -263,15 +267,19 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { override func updateState(size: CGSize, interfaceState: ChatPresentationInterfaceState) { let editMedia = interfaceState.editMessageState?.media var updatedEditMedia = false - if let currentEditMedia = self.currentEditMedia, let editMedia = editMedia { - if !currentEditMedia.isEqual(editMedia) { + if let currentEditMediaReference = self.currentEditMediaReference, let editMedia = editMedia { + if !currentEditMediaReference.media.isEqual(editMedia) { updatedEditMedia = true } - } else if (editMedia != nil) != (currentEditMedia != nil) { + } else if (editMedia != nil) != (self.currentEditMediaReference != nil) { updatedEditMedia = true } if updatedEditMedia { - self.currentEditMedia = editMedia + if let editMedia = editMedia { + self.currentEditMediaReference = .standalone(media: editMedia) + } else { + self.currentEditMediaReference = nil + } self.updateMessage(self.currentMessage) } } diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index f55149d676..46bf8c71a4 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -293,7 +293,7 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName var updateHiddenAvatarImpl: (() -> Void)? let wallpapersPromise = Promise<[TelegramWallpaper]>() - wallpapersPromise.set(telegramWallpapers(account: account)) + wallpapersPromise.set(telegramWallpapers(postbox: account.postbox, network: account.network)) let changeProfilePhotoImpl: () -> Void = { let _ = (account.postbox.transaction { transaction -> Peer? in diff --git a/TelegramUI/EditableTokenListNode.swift b/TelegramUI/EditableTokenListNode.swift index 006f07f02a..22e5361b1f 100644 --- a/TelegramUI/EditableTokenListNode.swift +++ b/TelegramUI/EditableTokenListNode.swift @@ -115,6 +115,8 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) self.textFieldNode.textField.textColor = theme.primaryTextColor + self.textFieldNode.textField.autocorrectionType = .no + self.textFieldNode.textField.returnKeyType = .done switch theme.keyboardColor { case .light: self.textFieldNode.textField.keyboardAppearance = .default diff --git a/TelegramUI/EmbedVideoNode.swift b/TelegramUI/EmbedVideoNode.swift deleted file mode 100644 index 2d0596e565..0000000000 --- a/TelegramUI/EmbedVideoNode.swift +++ /dev/null @@ -1,625 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -import LegacyComponents - -private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdge, view: TGEmbedPIPPullArrowView) { - let arrowX: CGFloat - switch edge { - case .left: - view.transform = .identity - arrowX = size.width - 40.0 + floor((40.0 - view.bounds.size.width) / 2.0) - case .right: - view.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) - arrowX = floor((40.0 - view.bounds.size.width) / 2.0) - } - - view.frame = CGRect(origin: CGPoint(x: arrowX, y: floor((size.height - view.bounds.size.height) / 2.0)), size: view.bounds.size) -} - -private final class SharedEmbedVideoContext: SharedVideoContext { - let playerView: TGEmbedPlayerView - let intrinsicSize: CGSize - - private let playbackCompletedListeners = Bag<() -> Void>() - - private let audioSessionDisposable = MetaDisposable() - private var hasAudioSession = false - - private let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - private let _preloadCompleted = ValuePromise() - var preloadCompleted: Signal { - return self._preloadCompleted.get() - } - - private let thumbnail = Promise() - private var thumbnailDisposable: Disposable? - - private var loadProgressDisposable: Disposable? - - init(account: Account, audioSessionManager: ManagedAudioSession, webpage: TelegramMediaWebpageLoadedContent) { - let converted = TGWebPageMediaAttachment() - - converted.url = webpage.url - converted.displayUrl = webpage.displayUrl - converted.pageType = webpage.type - converted.siteName = webpage.websiteName - converted.title = webpage.title - converted.pageDescription = webpage.text - converted.embedUrl = webpage.embedUrl - converted.embedType = webpage.embedType - converted.embedSize = webpage.embedSize ?? CGSize() - converted.duration = webpage.duration.flatMap { NSNumber.init(value: $0) } ?? 0 - converted.author = webpage.author - - if let embedSize = webpage.embedSize { - self.intrinsicSize = embedSize - } else { - self.intrinsicSize = CGSize(width: 480.0, height: 320.0) - } - - var thumbmnailSignal: SSignal? - if let _ = webpage.image { - let thumbnail = self.thumbnail - thumbmnailSignal = SSignal(generator: { subscriber in - let disposable = thumbnail.get().start(next: { image in - subscriber?.putNext(image) - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - } - - self.playerView = TGEmbedPlayerView.make(forWebPage: converted, thumbnailSignal: thumbmnailSignal)! - self.playerView.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) - self.playerView.disallowPIP = true - self.playerView.isUserInteractionEnabled = false - - super.init() - - let nativeLoadProgress = self.playerView.loadProgress() - let loadProgress: Signal = Signal { subscriber in - let disposable = nativeLoadProgress?.start(next: { value in - subscriber.putNext((value as! NSNumber).floatValue) - }) - return ActionDisposable { - disposable?.dispose() - } - } - self.loadProgressDisposable = (loadProgress |> deliverOnMainQueue).start(next: { [weak self] value in - if let strongSelf = self { - strongSelf._preloadCompleted.set(value.isEqual(to: 1.0)) - } - }) - - if let image = webpage.image { - self.thumbnailDisposable = (rawMessagePhoto(postbox: account.postbox, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in - if let strongSelf = self { - strongSelf.thumbnail.set(.single(image)) - strongSelf._ready.set(.single(Void())) - } - }) - } else { - self._ready.set(.single(Void())) - } - - self.playerView.stateSignal() - } - - deinit { - self.audioSessionDisposable.dispose() - - self.loadProgressDisposable?.dispose() - self.thumbnailDisposable?.dispose() - } - - func play() { - assert(Queue.mainQueue().isCurrent()) - self.playerView.playVideo() - } - - func pause() { - assert(Queue.mainQueue().isCurrent()) - self.playerView.pauseVideo() - } - - func togglePlayPause() { - assert(Queue.mainQueue().isCurrent()) - if let state = self.playerView.state, state.playing { - self.pause() - } else { - self.play() - } - } - - func seek(_ timestamp: Double) { - assert(Queue.mainQueue().isCurrent()) - self.playerView.seek(toPosition: timestamp) - } -} - -enum EmbedVideoNodeSource { - case webpage(TelegramMediaWebpageLoadedContent) - - fileprivate var id: EmbedVideoNodeMessageMediaId { - switch self { - case let .webpage(content): - return EmbedVideoNodeMessageMediaId(url: content.url) - } - } - - fileprivate var image: TelegramMediaImage? { - switch self { - case let .webpage(content): - return content.image - } - } -} - -private struct EmbedVideoNodeMessageMediaId: Hashable { - let url: String - - static func ==(lhs: EmbedVideoNodeMessageMediaId, rhs: EmbedVideoNodeMessageMediaId) -> Bool { - return lhs.url == rhs.url - } - - var hashValue: Int { - return self.url.hashValue - } -} - -private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayPlainVideoShadow")?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(top: 22.0, left: 25.0, bottom: 26.0, right: 25.0), resizingMode: .stretch) - -final class EmbedVideoNode: OverlayMediaItemNode { - private let manager: MediaManager - private let account: Account - private let source: EmbedVideoNodeSource - private let priority: Int32 - private let withSound: Bool - private let postbox: Postbox - - private var soundEnabled: Bool - - private var contextId: Int32? - - private var context: SharedEmbedVideoContext? - private var contextPlaybackEndedIndex: Int? - private var validLayout: CGSize? - - private let backgroundNode: ASImageNode - private let imageNode: TransformImageNode - private var snapshotView: UIView? - private var statusNode: RadialStatusNode? - private let controlsNode: PictureInPictureVideoControlsNode? - private var minimizedBlurView: UIVisualEffectView? - private var minimizedArrowView: TGEmbedPIPPullArrowView? - private var minimizedEdge: OverlayMediaItemMinimizationEdge? - - private var preloadDisposable: Disposable? - - var tapped: (() -> Void)? - var dismissed: (() -> Void)? - var unembed: (() -> Void)? - - private var initializedStatus = false - private let _status = Promise() - var status: Signal { - return self._status.get() - } - private let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - override var group: OverlayMediaItemNodeGroup? { - return OverlayMediaItemNodeGroup(rawValue: 1) - } - - override var isMinimizeable: Bool { - return true - } - - init(manager: MediaManager, account: Account, source: EmbedVideoNodeSource, priority: Int32, withSound: Bool, withOverlayControls: Bool = false) { - self.manager = manager - self.account = account - self.source = source - self.priority = priority - self.withSound = withSound - self.soundEnabled = withSound - self.postbox = account.postbox - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.imageNode = TransformImageNode() - - var leaveImpl: (() -> Void)? - var togglePlayPauseImpl: (() -> Void)? - var closeImpl: (() -> Void)? - - if withOverlayControls { - let controlsNode = PictureInPictureVideoControlsNode(leave: { - leaveImpl?() - }, playPause: { - togglePlayPauseImpl?() - }, close: { - closeImpl?() - }) - controlsNode.alpha = 0.0 - self.controlsNode = controlsNode - } else { - self.controlsNode = nil - } - - super.init() - - leaveImpl = { [weak self] in - self?.unembed?() - } - - togglePlayPauseImpl = { [weak self] in - self?.togglePlayPause() - } - - closeImpl = { [weak self] in - if let strongSelf = self { - if withOverlayControls { - strongSelf.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in - self?.dismiss() - }) - strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - } else { - strongSelf.dismiss() - } - } - } - - if withOverlayControls { - self.backgroundNode.image = backgroundImage - - self.layer.masksToBounds = true - self.layer.cornerRadius = 2.5 - } - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.imageNode) - if let controlsNode = self.controlsNode { - controlsNode.status = self.status - self.addSubnode(controlsNode) - } - - if let image = source.image { - self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image)) - } - } - - deinit { - if let context = self.context { - if context.playerView.superview === self.view { - context.playerView.removeFromSuperview() - } - } - - let manager = self.manager - let source = self.source - let contextId = self.contextId - - Queue.mainQueue().async { - if let contextId = contextId { - manager.sharedVideoContextManager.detachSharedVideoContext(id: source.id, index: contextId) - } - } - - self.preloadDisposable?.dispose() - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - private func updateContext(_ context: SharedEmbedVideoContext?) { - assert(Queue.mainQueue().isCurrent()) - - let previous = self.context - self.context = context - if previous !== context { - if let snapshotView = self.snapshotView { - snapshotView.removeFromSuperview() - self.snapshotView = nil - } - if let previous = previous { - self.contextPlaybackEndedIndex = nil - if previous.playerView.superview === self.view { - previous.playerView.removeFromSuperview() - } - } - if let context = context { - if context.playerView.superview !== self { - if let controlsNode = self.controlsNode { - self.view.insertSubview(context.playerView, belowSubview: controlsNode.view) - } else { - self.view.addSubview(context.playerView) - } - if let validLayout = self.validLayout { - self.updateLayoutImpl(validLayout) - } - } - } - if self.hasAttachedContext != (context !== nil) { - self.hasAttachedContext = (context !== nil) - self.hasAttachedContextUpdated?(self.hasAttachedContext) - } - } - } - - override func layout() { - self.updateLayout(self.bounds.size) - } - - override func updateLayout(_ size: CGSize) { - if size != self.validLayout { - self.updateLayoutImpl(size) - } - } - - private func updateLayoutImpl(_ size: CGSize) { - self.validLayout = size - - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()) - let videoFrame = CGRect(origin: CGPoint(), size: arguments.boundingSize) - - if let context = self.context { - context.playerView.center = CGPoint(x: videoFrame.midX, y: videoFrame.midY) - context.playerView.transform = CGAffineTransform(scaleX: videoFrame.size.width / context.intrinsicSize.width, y: videoFrame.size.height / context.intrinsicSize.height) - } - - let backgroundInsets = UIEdgeInsets(top: 11.5, left: 13.5, bottom: 11.5, right: 13.5) - self.backgroundNode.frame = CGRect(origin: CGPoint(x: -backgroundInsets.left, y: -backgroundInsets.top), size: CGSize(width: videoFrame.size.width + backgroundInsets.left + backgroundInsets.right, height: videoFrame.size.height + backgroundInsets.top + backgroundInsets.bottom)) - - self.imageNode.asyncLayout()(arguments)() - self.imageNode.frame = videoFrame - self.snapshotView?.frame = self.imageNode.frame - - if let statusNode = self.statusNode { - statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) - } - - if let controlsNode = self.controlsNode { - controlsNode.frame = videoFrame - controlsNode.updateLayout(size: videoFrame.size, transition: .immediate) - } - - if let minimizedBlurView = self.minimizedBlurView { - minimizedBlurView.frame = videoFrame - } - - if let minimizedArrowView = self.minimizedArrowView, let minimizedEdge = self.minimizedEdge { - setupArrowFrame(size: videoFrame.size, edge: minimizedEdge, view: minimizedArrowView) - } - } - - func play() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - context.play() - } - }) - } - - func pause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - context.pause() - } - }) - } - - func togglePlayPause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - context.togglePlayPause() - } - }) - } - - func setSoundEnabled(_ value: Bool) { - self.soundEnabled = value - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - //context.setSoundEnabled(value) - } - }) - } - - func seek(_ timestamp: Double) { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - context.seek(timestamp) - } - }) - } - - override func setShouldAcquireContext(_ value: Bool) { - if value { - if self.contextId == nil { - self.contextId = self.manager.sharedVideoContextManager.attachSharedVideoContext(id: source.id, priority: self.priority, create: { - switch self.source { - case let .webpage(content): - var size = CGSize(width: 100.0, height: 100.0) - if let embedSize = content.embedSize { - size = embedSize - } - let context = SharedEmbedVideoContext(account: self.account, audioSessionManager: manager.audioSession, webpage: content) - context.playerView.setup(withEmbedSize: size) - //context.setSoundEnabled(self.soundEnabled) - return context - } - }, update: { [weak self] context in - if let strongSelf = self { - strongSelf.updateContext(context as? SharedEmbedVideoContext) - } - }) - } - } else if let contextId = self.contextId { - self.manager.sharedVideoContextManager.detachSharedVideoContext(id: self.source.id, index: contextId) - self.contextId = nil - } - - if !self.initializedStatus { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedEmbedVideoContext { - self.initializedStatus = true - self._status.set(Signal { subscriber in - let innerDisposable = context.playerView.stateSignal().start(next: { next in - if let next = next as? TGEmbedPlayerState { - let status: MediaPlayerPlaybackStatus - if next.playing { - status = .playing - } else if next.downloadProgress.isEqual(to: 1.0) { - status = .buffering(initial: false, whilePlaying: next.playing) - } else { - status = .paused - } - subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, dimensions: CGSize(), timestamp: next.position, seekId: 0, status: status)) - } - }) - return ActionDisposable { - innerDisposable?.dispose() - } - }) - self._ready.set(context.ready) - - self.preloadDisposable = (context.preloadCompleted |> deliverOnMainQueue).start(next: { [weak self] value in - if let strongSelf = self { - if value { - if let statusNode = strongSelf.statusNode { - strongSelf.statusNode = nil - statusNode.transitionToState(.none, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - } - } else { - if strongSelf.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) - strongSelf.statusNode = statusNode - strongSelf.addSubnode(statusNode) - let size = strongSelf.bounds.size - statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) - statusNode.transitionToState(.progress(color: .white, value: nil, cancelEnabled: false), completion: {}) - } - } - } - }) - } - }) - } - } - - override func preferredSizeForOverlayDisplay() -> CGSize { - var size = CGSize(width: 100.0, height: 100.0) - switch self.source { - case let .webpage(content): - if let embedSize = content.embedSize { - size = embedSize - } - } - return size.aspectFitted(CGSize(width: 300.0, height: 300.0)) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?() - - if let controlsNode = self.controlsNode { - if controlsNode.alpha.isZero { - controlsNode.alpha = 1.0 - controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } else { - controlsNode.alpha = 0.0 - controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - - if let _ = self.minimizedEdge { - self.unminimize?() - } - } - } - - override func dismiss() { - self.dismissed?() - } - - override func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { - if self.minimizedEdge == edge { - if let minimizedArrowView = self.minimizedArrowView { - minimizedArrowView.setAngled(!adjusting, animated: true) - } - return - } - - self.minimizedEdge = edge - - if let edge = edge { - if self.minimizedBlurView == nil { - let minimizedBlurView = UIVisualEffectView(effect: nil) - self.minimizedBlurView = minimizedBlurView - minimizedBlurView.frame = self.bounds - minimizedBlurView.isHidden = true - self.view.addSubview(minimizedBlurView) - } - if self.minimizedArrowView == nil { - let minimizedArrowView = TGEmbedPIPPullArrowView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 8.0, height: 38.0))) - minimizedArrowView.alpha = 0.0 - self.minimizedArrowView = minimizedArrowView - self.minimizedBlurView?.contentView.addSubview(minimizedArrowView) - } - if let minimizedArrowView = self.minimizedArrowView { - setupArrowFrame(size: self.bounds.size, edge: edge, view: minimizedArrowView) - minimizedArrowView.setAngled(!adjusting, animated: true) - } - } - - let effect: UIBlurEffect? = edge != nil ? UIBlurEffect(style: .light) : nil - if true { - if let edge = edge { - self.minimizedBlurView?.isHidden = false - - switch edge { - case .left: - break - case .right: - break - } - } - - UIView.animate(withDuration: 0.35, animations: { - self.minimizedBlurView?.effect = effect - self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0; - }, completion: { [weak self] finished in - if let strongSelf = self { - if finished && edge == nil { - strongSelf.minimizedBlurView?.isHidden = true - } - } - }) - } else { - self.minimizedBlurView?.effect = effect; - self.minimizedBlurView?.isHidden = edge == nil - self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0 - } - } -} diff --git a/TelegramUI/EmojisChatInputContextPanelNode.swift b/TelegramUI/EmojisChatInputContextPanelNode.swift index a28c57b52e..0ac3d79e0b 100644 --- a/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -92,7 +92,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { let firstTime = self.currentEntries == nil let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.updateTextInputState { textInputState in + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var hashtagQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.emojiSearch] { @@ -113,9 +113,9 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { let selectionPosition = range.lowerBound + (replacementText as NSString).length - return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } - return textInputState + return (textInputState, inputMode) } } }) diff --git a/TelegramUI/FFMpegMediaFrameSource.swift b/TelegramUI/FFMpegMediaFrameSource.swift index 89db572226..4198bb0e96 100644 --- a/TelegramUI/FFMpegMediaFrameSource.swift +++ b/TelegramUI/FFMpegMediaFrameSource.swift @@ -68,7 +68,7 @@ private func contextForCurrentThread() -> FFMpegMediaFrameSourceContext? { final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let queue: Queue private let postbox: Postbox - private let resource: MediaResource + private let resourceReference: MediaResourceReference private let streamable: Bool private let video: Bool private let preferSoftwareDecoding: Bool @@ -91,10 +91,10 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - init(queue: Queue, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool) { + init(queue: Queue, postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool) { self.queue = queue self.postbox = postbox - self.resource = resource + self.resourceReference = resourceReference self.streamable = streamable self.video = video self.preferSoftwareDecoding = preferSoftwareDecoding @@ -145,7 +145,7 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.generatingFrames = true let postbox = self.postbox - let resource = self.resource + let resourceReference = self.resourceReference let queue = self.queue let streamable = self.streamable let video = self.video @@ -153,7 +153,7 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let fetchAutomatically = self.fetchAutomatically self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically) + context.initializeState(postbox: postbox, resourceReference: resourceReference, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically) let (frames, endOfStream) = context.takeFrames(until: timestamp) @@ -194,14 +194,14 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let queue = self.queue let postbox = self.postbox - let resource = self.resource + let resourceReference = self.resourceReference let streamable = self.streamable let video = self.video let preferSoftwareDecoding = self.preferSoftwareDecoding let fetchAutomatically = self.fetchAutomatically self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically) + context.initializeState(postbox: postbox, resourceReference: resourceReference, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically) context.seek(timestamp: timestamp, completed: { streamDescriptions, timestamp in queue.async { diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 7e0e126101..3a4a06c520 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -66,7 +66,7 @@ struct FFMpegMediaFrameSourceContextInfo { private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let postbox = context.postbox, let resource = context.resource, let streamable = context.streamable else { + guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable else { return 0 } @@ -76,9 +76,9 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa if streamable { let data: Signal - let resourceSize: Int = resource.size ?? Int(Int32.max - 1) + let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) let readCount = min(resourceSize - context.readingOffset, Int(bufferSize)) - data = postbox.mediaBox.resourceData(resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) + data = postbox.mediaBox.resourceData(resourceReference.resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) let semaphore = DispatchSemaphore(value: 0) if readCount == 0 { fetchedData = Data() @@ -93,7 +93,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa disposable.dispose() } } else { - let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)) + let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)) let semaphore = DispatchSemaphore(value: 0) let disposable = data.start(next: { next in if next.complete { @@ -130,19 +130,19 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let postbox = context.postbox, let resource = context.resource, let streamable = context.streamable, let fetchTag = context.fetchTag else { + guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable, let statsCategory = context.statsCategory else { return 0 } var result: Int64 = offset let resourceSize: Int - if let size = resource.size { + if let size = resourceReference.resource.size { resourceSize = size } else { if !streamable { var resultSize: Int = Int(Int32.max - 1) - let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)) + let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)) let semaphore = DispatchSemaphore(value: 0) let disposable = data.start(next: { next in if next.complete { @@ -171,10 +171,10 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe context.requestedCompleteFetch = false } else { if streamable { - context.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, in: context.readingOffset ..< resourceSize, tag: fetchTag).start()) + context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: context.readingOffset ..< resourceSize, statsCategory: statsCategory).start()) } else if !context.requestedCompleteFetch && context.fetchAutomatically { context.requestedCompleteFetch = true - context.fetchedDataDisposable.set(postbox.mediaBox.fetchedResource(resource, tag: fetchTag).start()) + context.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory).start()) } } } @@ -189,9 +189,9 @@ final class FFMpegMediaFrameSourceContext: NSObject { var closed = false fileprivate var postbox: Postbox? - fileprivate var resource: MediaResource? + fileprivate var resourceReference: MediaResourceReference? fileprivate var streamable: Bool? - fileprivate var fetchTag: TelegramMediaResourceFetchTag? + fileprivate var statsCategory: MediaResourceStatsCategory? private let ioBufferSize = 64 * 1024 fileprivate var readingOffset = 0 @@ -218,7 +218,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fetchedDataDisposable.dispose() } - func initializeState(postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool) { + func initializeState(postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool) { if self.readingError || self.initializedState != nil { return } @@ -226,23 +226,19 @@ final class FFMpegMediaFrameSourceContext: NSObject { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals self.postbox = postbox - self.resource = resource + self.resourceReference = resourceReference self.streamable = streamable + self.statsCategory = video ? .video : .audio self.preferSoftwareDecoding = preferSoftwareDecoding self.fetchAutomatically = fetchAutomatically - if video { - self.fetchTag = TelegramMediaResourceFetchTag(statsCategory: .video) - } else { - self.fetchTag = TelegramMediaResourceFetchTag(statsCategory: .audio) - } - let resourceSize: Int = resource.size ?? Int(Int32.max - 1) + let resourceSize: Int = resourceReference.resource.size ?? Int(Int32.max - 1) if streamable { - self.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, in: 0 ..< resourceSize, tag: self.fetchTag).start()) + self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, range: 0 ..< resourceSize, statsCategory: self.statsCategory ?? .generic).start()) } else if !self.requestedCompleteFetch && self.fetchAutomatically { self.requestedCompleteFetch = true - self.fetchedDataDisposable.set(postbox.mediaBox.fetchedResource(resource, tag: self.fetchTag).start()) + self.fetchedDataDisposable.set(fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic).start()) } var avFormatContextRef = avformat_alloc_context() diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index d73f00c323..08fa0f42e3 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -171,7 +171,22 @@ public func featuredStickerPacksController(account: Account) -> ViewController { let arguments = FeaturedStickerPacksControllerArguments(account: account, openStickerPack: { info in presentStickerPackController?(info) }, addPack: { info in - presentStickerPackController?(info) + let _ = (loadedStickerPack(postbox: account.postbox, network: account.network, reference: .id(id: info.id.id, accessHash: info.accessHash)) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return addStickerPackInteractively(postbox: account.postbox, info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } |> deliverOnMainQueue).start() }) let stickerPacks = Promise() diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 9a9e2c83d5..dc06bdb324 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -115,7 +115,7 @@ private func fetchCachedScaledImageRepresentation(account: Account, resource: Me let colorImage = generateImage(size, contextGenerator: { size, context in context.setBlendMode(.copy) - context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + drawImage(context: context, image: image.cgImage!, orientation: image.imageOrientation, in: CGRect(origin: CGPoint(), size: size)) }, scale: 1.0)! if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { diff --git a/TelegramUI/FetchManager.swift b/TelegramUI/FetchManager.swift index 2f2de9f9cf..759adc9699 100644 --- a/TelegramUI/FetchManager.swift +++ b/TelegramUI/FetchManager.swift @@ -30,26 +30,27 @@ private struct FetchManagerLocationEntryId: Hashable { private final class FetchManagerLocationEntry { let id: FetchManagerLocationEntryId let episode: Int32 - let resource: MediaResource - let fetchTag: MediaResourceFetchTag? + let resourceReference: MediaResourceReference + let statsCategory: MediaResourceStatsCategory + var userInitiated: Bool = false var referenceCount: Int32 = 0 var elevatedPriorityReferenceCount: Int32 = 0 var userInitiatedPriorityIndices: [Int32] = [] var priorityKey: FetchManagerPriorityKey? { - if self.referenceCount >= 0 { + if self.referenceCount > 0 || self.userInitiated { return FetchManagerPriorityKey(locationKey: self.id.locationKey, hasElevatedPriority: self.elevatedPriorityReferenceCount > 0, userInitiatedPriority: userInitiatedPriorityIndices.last) } else { return nil } } - init(id: FetchManagerLocationEntryId, episode: Int32, resource: MediaResource, fetchTag: MediaResourceFetchTag?) { + init(id: FetchManagerLocationEntryId, episode: Int32, resourceReference: MediaResourceReference, statsCategory: MediaResourceStatsCategory) { self.id = id self.episode = episode - self.resource = resource - self.fetchTag = fetchTag + self.resourceReference = resourceReference + self.statsCategory = statsCategory } } @@ -96,7 +97,7 @@ private final class FetchManagerCategoryContext { self.entryCompleted = entryCompleted } - func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (MediaResource, MediaResourceFetchTag?, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) { + func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) { let entry: FetchManagerLocationEntry let previousPriorityKey: FetchManagerPriorityKey? @@ -105,8 +106,8 @@ private final class FetchManagerCategoryContext { previousPriorityKey = entry.priorityKey } else if let takeNew = takeNew { previousPriorityKey = nil - let (resource, fetchTag, episode) = takeNew() - entry = FetchManagerLocationEntry(id: id, episode: episode, resource: resource, fetchTag: fetchTag) + let (resourceReference, statsCategory, episode) = takeNew() + entry = FetchManagerLocationEntry(id: id, episode: episode, resourceReference: resourceReference, statsCategory: statsCategory) self.entries[id] = entry } else { return @@ -156,7 +157,7 @@ private final class FetchManagerCategoryContext { if activeContext.disposable == nil { if let entry = self.entries[id] { let entryCompleted = self.entryCompleted - activeContext.disposable = self.postbox.mediaBox.fetchedResource(entry.resource, tag: entry.fetchTag, implNext: true).start(next: { value in + activeContext.disposable = fetchedMediaResource(postbox: self.postbox, reference: entry.resourceReference, statsCategory: entry.statsCategory, reportResultStatus: true).start(next: { value in entryCompleted(id) }) } else { @@ -221,7 +222,7 @@ private final class FetchManagerCategoryContext { let activeContext = FetchManagerActiveContext() self.activeContexts[topEntryId] = activeContext let entryCompleted = self.entryCompleted - activeContext.disposable = self.postbox.mediaBox.fetchedResource(entry.resource, tag: entry.fetchTag, implNext: true).start(next: { value in + activeContext.disposable = fetchedMediaResource(postbox: self.postbox, reference: entry.resourceReference, statsCategory: entry.statsCategory, reportResultStatus: true).start(next: { value in entryCompleted(topEntryId) }) } else { @@ -347,7 +348,7 @@ final class FetchManager { } } - func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource, fetchTag: MediaResourceFetchTag?, elevatedPriority: Bool, userInitiated: Bool) -> Signal { + func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resourceReference: MediaResourceReference, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool) -> Signal { let queue = self.queue return Signal { [weak self] subscriber in if let strongSelf = self { @@ -355,8 +356,11 @@ final class FetchManager { var assignedUserInitiatedIndex: Int32? strongSelf.withCategoryContext(category, { context in - context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: resource.id, locationKey: locationKey), takeNew: { return (resource, fetchTag, strongSelf.takeNextEpisodeId()) }, { entry in + context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: resourceReference.resource.id, locationKey: locationKey), takeNew: { return (resourceReference, statsCategory, strongSelf.takeNextEpisodeId()) }, { entry in assignedEpisode = entry.episode + if userInitiated { + entry.userInitiated = true + } entry.referenceCount += 1 if elevatedPriority { entry.elevatedPriorityReferenceCount += 1 @@ -374,7 +378,7 @@ final class FetchManager { queue.async { if let strongSelf = self { strongSelf.withCategoryContext(category, { context in - context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: resource.id, locationKey: locationKey), takeNew: nil, { entry in + context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: resourceReference.resource.id, locationKey: locationKey), takeNew: nil, { entry in if entry.episode == assignedEpisode { entry.referenceCount -= 1 assert(entry.referenceCount >= 0) diff --git a/TelegramUI/FetchMediaUtils.swift b/TelegramUI/FetchMediaUtils.swift index 7ca96f242c..0231a59475 100644 --- a/TelegramUI/FetchMediaUtils.swift +++ b/TelegramUI/FetchMediaUtils.swift @@ -3,16 +3,16 @@ import TelegramCore import Postbox import SwiftSignalKit -func freeMediaFileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal { - return account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: file.isVideo ? .video : .file)) +func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { + return fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)) } func cancelFreeMediaFileInteractiveFetch(account: Account, file: TelegramMediaFile) { account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } -func messageMediaFileInteractiveFetched(account: Account, messageId: MessageId, file: TelegramMediaFile) -> Signal { - return account.telegramApplicationContext.fetchManager.interactivelyFetched(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource, fetchTag: TelegramMediaResourceFetchTag(statsCategory: file.isVideo ? .video : .file), elevatedPriority: false, userInitiated: true) +func messageMediaFileInteractiveFetched(account: Account, message: Message, file: TelegramMediaFile) -> Signal { + return account.telegramApplicationContext.fetchManager.interactivelyFetched(category: .file, location: .chat(message.id.peerId), locationKey: .messageId(message.id), resourceReference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: true) } func messageMediaFileCancelInteractiveFetch(account: Account, messageId: MessageId, file: TelegramMediaFile) { diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 1f51cceaaf..46dbef6b5e 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -128,7 +128,7 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else if let file = media as? TelegramMediaFile { if file.isVideo || file.mimeType.hasPrefix("video/") { - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), file: file, streamVideo: streamVideos, loopVideo: loopVideos), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted) + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .message(message.id, message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted) } else { if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { if file.size == nil || file.size! < 5 * 1024 * 1024 { @@ -145,12 +145,12 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content { switch websiteType(of: webpageContent) { case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: NativeVideoContentId.message(message.id, message.stableId, webpage.webpageId), file: webpageContent.file!, streamVideo: true, enableSound: true), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: NativeVideoContentId.message(message.id, message.stableId, webpage.webpageId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), streamVideo: true, enableSound: true), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") //return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: SystemVideoContent(url: webpageContent.embedUrl!, image: webpageContent.image!, dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") /*case .twitter where webpageContent.embedUrl != nil && webpageContent.image != nil: return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: SystemVideoContent(url: webpageContent.embedUrl!, image: webpageContent.image!, dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "")*/ default: - if let content = WebEmbedVideoContent(webpageContent: webpageContent) { + if let content = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent) { return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "") } } diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index 91098a5e41..eebd567af6 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -23,6 +23,8 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog private var presentationState = GalleryControllerPresentationState() + private var isDismissed = false + var areControlsHidden = false var isBackgroundExtendedOverNavigationBar = true { didSet { @@ -242,6 +244,8 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog } func animateOut(animateContent: Bool, completion: @escaping () -> Void) { + self.isDismissed = true + var contentAnimationCompleted = true var interfaceAnimationCompleted = false @@ -284,6 +288,9 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog } func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.isDismissed { + return + } let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 let transition = 1.0 - min(1.0, max(0.0, abs(distanceFromEquilibrium) / 50.0)) diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index c63e8f01d7..f4d1b11940 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -186,7 +186,8 @@ final class GridMessageItemNode: GridItemNode { var mediaDimensions: CGSize? if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize - self.imageNode.setSignal(mediaGridMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: true) + + self.imageNode.setSignal(mediaGridMessagePhoto(account: account, photoReference: .message(message: MessageReference(item.message), media: image)), dispatchOnDisplayLink: true) self.fetchStatusDisposable.set(nil) self.statusNode.transitionToState(.none, completion: { [weak self] in @@ -195,7 +196,7 @@ final class GridMessageItemNode: GridItemNode { self.resourceStatus = nil } else if let file = media as? TelegramMediaFile, file.isVideo { mediaDimensions = file.dimensions - self.imageNode.setSignal(mediaGridMessageVideo(postbox: account.postbox, video: file)) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: account.postbox, videoReference: .message(message: MessageReference(item.message), media: file))) self.resourceStatus = nil self.fetchStatusDisposable.set((messageMediaFileStatus(account: account, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in @@ -344,7 +345,7 @@ final class GridMessageItemNode: GridItemNode { case .Local: let _ = controllerInteraction.openMessage(message) case .Remote: - self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: message.id, file: file).start()) + self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file).start()) } } } else { diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index a1d084de09..46c0f717a2 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1348,64 +1348,64 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } let addMember = contactsController.result - |> deliverOnMainQueue - |> mapToSignal { memberId -> Signal in - if let memberId = memberId { - if peerId.namespace == Namespaces.Peer.CloudChannel { - return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.addMember(account: account, peerId: peerId, memberId: memberId) - } - - return account.postbox.peerView(id: memberId) - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { view -> Signal in - if let peer = view.peers[memberId] { - updateState { state in - var found = false - for participant in state.temporaryParticipants { - if participant.peer.id == memberId { - found = true - break - } - } - if !found { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - var temporaryParticipants = state.temporaryParticipants - temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.peerPresences[memberId], timestamp: timestamp)) - return state.withUpdatedTemporaryParticipants(temporaryParticipants) - } else { - return state - } + |> deliverOnMainQueue + |> mapToSignal { memberId -> Signal in + if let memberId = memberId { + if peerId.namespace == Namespaces.Peer.CloudChannel { + return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.addMember(account: account, peerId: peerId, memberId: memberId) + } + + return account.postbox.peerView(id: memberId) + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { view -> Signal in + if let peer = view.peers[memberId] { + updateState { state in + var found = false + for participant in state.temporaryParticipants { + if participant.peer.id == memberId { + found = true + break } } - - return addPeerMember(account: account, peerId: peerId, memberId: memberId) - |> deliverOnMainQueue - |> afterCompleted { - updateState { state in - var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds - successfullyAddedParticipantIds.insert(memberId) - - return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) - } - } |> `catch` { _ -> Signal in - updateState { state in - var temporaryParticipants = state.temporaryParticipants - for i in 0 ..< temporaryParticipants.count { - if temporaryParticipants[i].peer.id == memberId { - temporaryParticipants.remove(at: i) - break - } - } - var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds - successfullyAddedParticipantIds.remove(memberId) - - return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) - } - - return .complete() - } + if !found { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var temporaryParticipants = state.temporaryParticipants + temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.peerPresences[memberId], timestamp: timestamp)) + return state.withUpdatedTemporaryParticipants(temporaryParticipants) + } else { + return state + } } + } + + return addPeerMember(account: account, peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterCompleted { + updateState { state in + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.insert(memberId) + + return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + } |> `catch` { _ -> Signal in + updateState { state in + var temporaryParticipants = state.temporaryParticipants + for i in 0 ..< temporaryParticipants.count { + if temporaryParticipants[i].peer.id == memberId { + temporaryParticipants.remove(at: i) + break + } + } + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.remove(memberId) + + return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + + return .complete() + } + } } else { return .complete() } diff --git a/TelegramUI/GroupStickerPackCurrentItem.swift b/TelegramUI/GroupStickerPackCurrentItem.swift index d2f149d4f7..be03f08292 100644 --- a/TelegramUI/GroupStickerPackCurrentItem.swift +++ b/TelegramUI/GroupStickerPackCurrentItem.swift @@ -123,11 +123,13 @@ class GroupStickerPackCurrentItemNode: ItemListRevealOptionsItemNode { self.titleNode = TextNode() self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale self.statusNode = TextNode() self.statusNode.isLayerBacked = true + self.statusNode.displaysAsynchronously = false self.statusNode.contentMode = .left self.statusNode.contentsScale = UIScreen.main.scale @@ -223,7 +225,7 @@ class GroupStickerPackCurrentItemNode: ItemListRevealOptionsItemNode { if fileUpdated { if let file = file { updatedImageSignal = chatMessageSticker(account: item.account, file: file, small: false) - updatedFetchSignal = item.account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)) + updatedFetchSignal = fetchedMediaResource(postbox: item.account.postbox, reference: stickerPackFileReference(file).resourceReference(file.resource)) } else { updatedImageSignal = .single({ _ in return nil }) updatedFetchSignal = .complete() diff --git a/TelegramUI/GroupStickerPackSetupController.swift b/TelegramUI/GroupStickerPackSetupController.swift index 478ac2f029..2220f25caa 100644 --- a/TelegramUI/GroupStickerPackSetupController.swift +++ b/TelegramUI/GroupStickerPackSetupController.swift @@ -199,8 +199,16 @@ private enum GroupStickerPackEntry: ItemListNodeEntry { func item(_ arguments: GroupStickerPackSetupControllerArguments) -> ListViewItem { switch self { case let .search(theme, prefix, placeholder, value): - return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: prefix), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearButton: true, tag: nil, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearButton: true, tag: nil, sectionId: self.section, textUpdated: { value in arguments.updateSearchText(value) + }, processPaste: { text in + if let url = (URL(string: text) ?? URL(string: "http://" + text)), url.host == "t.me" || url.host == "telegram.me" { + let prefix = "/addstickers/" + if url.path.hasPrefix(prefix) { + return String(url.path[url.path.index(url.path.startIndex, offsetBy: prefix.count)...]) + } + } + return text }, action: {}) case let .searchInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section, linkAction: nil) @@ -341,7 +349,7 @@ public func groupStickerPackSetupController(account: Account, peerId: PeerId, cu } } return .single((searchText, .searching)) - |> then((loadedStickerPack(postbox: account.postbox, network: account.network, reference: .name(searchText.lowercased())) |> delay(0.1, queue: Queue.concurrentDefaultQueue())) + |> then((loadedStickerPack(postbox: account.postbox, network: account.network, reference: .name(searchText.lowercased())) |> delay(0.3, queue: Queue.concurrentDefaultQueue())) |> mapToSignal { value -> Signal<(String, GroupStickerPackSearchState), NoError> in switch value { case .fetching: @@ -360,6 +368,7 @@ public func groupStickerPackSetupController(account: Account, peerId: PeerId, cu var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var navigateToChatControllerImpl: ((PeerId) -> Void)? + var dismissInputImpl: (() -> Void)? var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -458,7 +467,11 @@ public func groupStickerPackSetupController(account: Account, peerId: PeerId, cu controller.present(c, in: .window(.root), with: p) } } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } presentStickerPackController = { [weak controller] info in + dismissInputImpl?() presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } navigateToChatControllerImpl = { [weak controller] peerId in @@ -467,6 +480,7 @@ public func groupStickerPackSetupController(account: Account, peerId: PeerId, cu } } dismissImpl = { [weak controller] in + dismissInputImpl?() controller?.dismiss() } diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index ab224f8ad7..1a5585e6f3 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -98,7 +98,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { let firstTime = self.currentEntries == nil let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.updateTextInputState { textInputState in + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var hashtagQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.hashtag] { @@ -116,9 +116,9 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { let selectionPosition = range.lowerBound + (replacementText as NSString).length - return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } - return textInputState + return (textInputState, inputMode) } } }) diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 7595df59d6..fc2bec2016 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -140,7 +140,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode init() { self.imageNodeBackground = ASDisplayNode() self.imageNodeBackground.isLayerBacked = true - self.imageNodeBackground.backgroundColor = UIColor(white: 0.9, alpha: 1.0) self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] @@ -192,6 +191,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var imageResource: TelegramMediaResource? + var stickerFile: TelegramMediaFile? var videoFile: TelegramMediaFile? var imageDimensions: CGSize? switch item.result { @@ -203,7 +203,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } imageDimensions = content?.dimensions if type == "gif", let thumbnailResource = imageResource, let content = content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), reference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) imageResource = nil } case let .internalReference(_, _, title, _, image, file, _): @@ -218,7 +218,12 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { imageDimensions = largestRepresentation.dimensions } - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource + if file.isSticker { + stickerFile = file + imageResource = file.resource + } else { + imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource + } } if let file = file { @@ -265,10 +270,13 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if updatedImageResource { if let imageResource = imageResource { - let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) - //updateImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) - updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photo: tmpImage) + if let stickerFile = stickerFile { + updateImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false) + } else { + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) + updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage)) + } } else { updateImageSignal = .complete() } @@ -302,14 +310,14 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } if let videoFile = videoFile { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, file: videoFile) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile)) thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: item.account, resource: videoFile.resource, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: item.account, fileReference: .standalone(media: videoFile), resource: videoFile.resource, layerHolder: layerHolder) strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { diff --git a/TelegramUI/HorizontalStickerGridItem.swift b/TelegramUI/HorizontalStickerGridItem.swift index 513de8ff16..0dc65e61ce 100644 --- a/TelegramUI/HorizontalStickerGridItem.swift +++ b/TelegramUI/HorizontalStickerGridItem.swift @@ -78,7 +78,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.file.id != item.file.id { if let dimensions = item.file.dimensions { self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: item.file).start()) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file)).start()) self.currentState = (account, item, dimensions) self.setNeedsLayout() @@ -105,7 +105,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - interfaceInteraction.sendSticker(item.file) + interfaceInteraction.sendSticker(.standalone(media: item.file)) } } diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift index 90c8ecb06f..20528a6cda 100644 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -202,7 +202,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { transition.updateFrame(node: self.backgroundRightNode, frame: backgroundRightFrame) let gridFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 4.0), size: CGSize(width: backgroundFrame.size.width, height: 66.0)) - self.clippingNode.frame = gridFrame + transition.updateFrame(node: self.clippingNode, frame: gridFrame) self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width)) let gridBounds = self.gridNode.bounds @@ -227,6 +227,9 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.clippingNode.frame.contains(point) { + return nil + } return super.hitTest(point, with: event) } diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 6d060d4d8f..d9c3328f24 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -320,12 +320,19 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati } } + var markdownString: String switch mode { case .general, .modal: - entries.append(.packsInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_ManagingHelp)) + markdownString = presentationData.strings.StickerPacksSettings_ManagingHelp case .masks: - entries.append(.packsInfo(presentationData.theme, presentationData.strings.MaskStickerSettings_Info)) + markdownString = presentationData.strings.MaskStickerSettings_Info } + let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) + if let entity = entities.first { + markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) + markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) + } + entries.append(.packsInfo(presentationData.theme, markdownString)) return entries } @@ -374,13 +381,14 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } controller.setItemGroups([ ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.StickerSettings_ContextInfo), + ActionSheetButtonItem(title: presentationData.strings.StickerSettings_ContextHide, color: .accent, action: { + dismissAction() + let _ = removeStickerPackInteractively(postbox: account.postbox, id: id, option: .archive).start() + }), ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { dismissAction() let _ = removeStickerPackInteractively(postbox: account.postbox, id: id, option: .delete).start() - }), - ActionSheetButtonItem(title: presentationData.strings.StickerSettings_ContextHide, color: .destructive, action: { - dismissAction() - let _ = removeStickerPackInteractively(postbox: account.postbox, id: id, option: .archive).start() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index ca03e57f84..8318cdcf40 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -7,20 +7,21 @@ import TelegramCore private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem { let account: Account - let representations: [TelegramMediaImageRepresentation] + let mediaReference: AnyMediaReference var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: self.representations, reference: nil) - if let representation = largestImageRepresentation(image.representations) { - return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions) - } else { + if let imageReferene = mediaReference.concrete(TelegramMediaImage.self), let representation = largestImageRepresentation(imageReferene.media.representations) { + return (mediaGridMessagePhoto(account: self.account, photoReference: imageReferene), representation.dimensions) + } else if let fileReference = mediaReference.concrete(TelegramMediaFile.self), let dimensions = fileReference.media.dimensions { + return (mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference), dimensions) + } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } } func isEqual(to: GalleryThumbnailItem) -> Bool { if let to = to as? InstantImageGalleryThumbnailItem { - return self.representations == to.representations + return self.mediaReference == to.mediaReference } else { return false } @@ -31,15 +32,15 @@ class InstantImageGalleryItem: GalleryItem { let account: Account let theme: PresentationTheme let strings: PresentationStrings - let image: TelegramMediaImage + let imageReference: ImageMediaReference let caption: String let location: InstantPageGalleryEntryLocation - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, image: TelegramMediaImage, caption: String, location: InstantPageGalleryEntryLocation) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, imageReference: ImageMediaReference, caption: String, location: InstantPageGalleryEntryLocation) { self.account = account self.theme = theme self.strings = strings - self.image = image + self.imageReference = imageReference self.caption = caption self.location = location } @@ -47,7 +48,7 @@ class InstantImageGalleryItem: GalleryItem { func node() -> GalleryItemNode { let node = InstantImageGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) - node.setImage(image: self.image) + node.setImage(imageReference: self.imageReference) node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)")) @@ -65,7 +66,7 @@ class InstantImageGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return (0, InstantImageGalleryThumbnailItem(account: self.account, representations: self.image.representations)) + return (0, InstantImageGalleryThumbnailItem(account: self.account, mediaReference: imageReference.abstract)) } } @@ -77,7 +78,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _title = Promise() private let footerContentNode: InstantPageGalleryFooterContentNode - private var accountAndMedia: (Account, Media)? + private var accountAndMedia: (Account, AnyMediaReference)? private var fetchDisposable = MetaDisposable() @@ -113,33 +114,33 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setCaption(caption) } - fileprivate func setImage(image: TelegramMediaImage) { - if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) { - if let largestSize = largestRepresentationForPhoto(image) { + fileprivate func setImage(imageReference: ImageMediaReference) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(imageReference.media) { + if let largestSize = largestRepresentationForPhoto(imageReference.media) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photo: image), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: imageReference.resourceReference(largestSize.resource)).start()) } else { self._ready.set(.single(Void())) } } - self.accountAndMedia = (account, image) + self.accountAndMedia = (account, imageReference.abstract) } - func setFile(account: Account, file: TelegramMediaFile) { - if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(file) { - if let largestSize = file.dimensions { + func setFile(account: Account, fileReference: FileMediaReference) { + if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(fileReference.media) { + if let largestSize = fileReference.media.dimensions { let displaySize = largestSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatMessageImageFile(account: account, file: file, thumbnail: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: account, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) } else { self._ready.set(.single(Void())) } } - self.accountAndMedia = (account, file) + self.accountAndMedia = (account, fileReference.abstract) } override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { @@ -216,9 +217,9 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) - if let (account, media) = self.accountAndMedia, let file = media as? TelegramMediaFile { + if let (account, media) = self.accountAndMedia, let fileReference = media.concrete(TelegramMediaFile.self) { if isVisible { - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) } else { self.fetchDisposable.set(nil) } diff --git a/TelegramUI/InstantPageAudioItem.swift b/TelegramUI/InstantPageAudioItem.swift index 47b882fb2b..db651fd9da 100644 --- a/TelegramUI/InstantPageAudioItem.swift +++ b/TelegramUI/InstantPageAudioItem.swift @@ -19,7 +19,7 @@ final class InstantPageAudioItem: InstantPageItem { } func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageAudioNode(account: account, strings: strings, theme: theme, webpage: self.webpage, media: self.media, openMedia: openMedia) + return InstantPageAudioNode(account: account, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPageAudioNode.swift b/TelegramUI/InstantPageAudioNode.swift index 9fc87061be..7fc1ff5dbb 100644 --- a/TelegramUI/InstantPageAudioNode.swift +++ b/TelegramUI/InstantPageAudioNode.swift @@ -57,6 +57,8 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { private var strings: PresentationStrings private var theme: InstantPageTheme + private let playlistType: MediaManagerPlayerType + private var playImage: UIImage private var pauseImage: UIImage @@ -68,9 +70,9 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { private var playerStatusDisposable: Disposable? private var isPlaying: Bool = false - private var playlistStateAndStatus: AudioPlaylistStateAndStatus? + private var playbackState: SharedMediaPlayerItemPlaybackState? - init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, webpage: TelegramMediaWebpage, media: InstantPageMedia, openMedia: @escaping (InstantPageMedia) -> Void) { + init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, openMedia: @escaping (InstantPageMedia) -> Void) { self.account = account self.strings = strings self.theme = theme @@ -93,6 +95,14 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color)) + let playlistType: MediaManagerPlayerType + if let file = self.media.media as? TelegramMediaFile { + playlistType = file.isVoice ? .voice : .music + } else { + playlistType = .music + } + self.playlistType = playlistType + super.init() self.titleNode.attributedText = titleString(media: media, theme: theme) @@ -119,13 +129,13 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { self.scrubbingNode.seek = { [weak self] timestamp in if let strongSelf = self { - if let _ = strongSelf.playlistStateAndStatus { - strongSelf.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.seek(timestamp))) + if let _ = strongSelf.playbackState { + strongSelf.account.telegramApplicationContext.mediaManager.playlistControl(.seek(timestamp), type: strongSelf.playlistType) } } } - if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = instantPageAudioPlaylistAndItemIds(webpage: webpage, media: self.media) { + /*if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = instantPageAudioPlaylistAndItemIds(webpage: webpage, media: self.media) { let playbackStatus: Signal = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId) |> mapToSignal { status -> Signal in if let status = status, let playbackStatus = status.status { @@ -139,8 +149,8 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } else { return .single(nil) } - } - self.playbackStatusDisposable = (playbackStatus |> deliverOnMainQueue).start(next: { [weak self] status in + }*/ + /*self.playbackStatusDisposable = (playbackStatus |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { var isPlaying = false if let status = status { @@ -162,31 +172,41 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } } } - }) - self.playerStatusDisposable = (applicationContext.mediaManager.playlistPlayerStateAndStatus - |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndStatus in - if let strongSelf = self { - var filteredValue: AudioPlaylistStateAndStatus? - if let playlistStateAndStatus = playlistStateAndStatus { - if playlistStateAndStatus.state.playlistId.isEqual(to: playlistId) { - if let item = playlistStateAndStatus.state.item { - if item.id.isEqual(to: itemId) { - filteredValue = playlistStateAndStatus - } - } - } - } - if strongSelf.playlistStateAndStatus != filteredValue { - strongSelf.playlistStateAndStatus = filteredValue - strongSelf.scrubbingNode.status = filteredValue?.status - } - } - }) + })*/ + + self.scrubbingNode.status = account.telegramApplicationContext.mediaManager.filteredPlaylistState(playlistId: InstantPageMediaPlaylistId(webpageId: webPage.webpageId), itemId: InstantPageMediaPlaylistItemId(index: self.media.index), type: self.playlistType) + |> map { playbackState -> MediaPlayerStatus in + return playbackState?.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } + + self.playerStatusDisposable = (account.telegramApplicationContext.mediaManager.filteredPlaylistState(playlistId: InstantPageMediaPlaylistId(webpageId: webPage.webpageId), itemId: InstantPageMediaPlaylistItemId(index: self.media.index), type: playlistType) + |> deliverOnMainQueue).start(next: { [weak self] playbackState in + guard let strongSelf = self else { + return + } + strongSelf.playbackState = playbackState + let isPlaying: Bool + if let status = playbackState?.status { + if case .playing = status.status { + isPlaying = true + } else { + isPlaying = false + } + } else { + isPlaying = false + } + if strongSelf.isPlaying != isPlaying { + strongSelf.isPlaying = isPlaying + if isPlaying { + strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.pauseImage), animated: false, completion: {}) + } else { + strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.playImage), animated: false, completion: {}) + } + } + }) } deinit { - self.playbackStatusDisposable?.dispose() self.playerStatusDisposable?.dispose() } @@ -225,11 +245,8 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } @objc func buttonPressed() { - if let _ = self.playlistStateAndStatus { - if self.isPlaying { self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.pause)) - } else { - self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.play)) - } + if let _ = self.playbackState { + self.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: self.playlistType) } else { self.openMedia(self.media) } diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 902bbbab41..e3b2b06051 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -658,6 +658,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text + }), ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuShare), action: { [weak self] in + if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + strongSelf.present(ShareController(account: strongSelf.account, subject: .quote(text: text, url: content.url)), nil) + } })]) controller.dismissed = { [weak self] in self?.updateTextSelectionRects([], text: nil) @@ -746,16 +750,18 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) { var medias: [InstantPageMedia] = [] + var initialIndex = 0 for item in items { for itemMedia in item.medias { if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) { + if itemMedia.index == media.index { + initialIndex = medias.count + } medias.append(itemMedia) } } } - let player = ManagedAudioPlaylistPlayer(audioSessionManager: self.account.telegramApplicationContext.mediaManager.audioSession, overlayMediaManager: self.account.telegramApplicationContext.mediaManager.overlayMediaManager, mediaManager: self.account.telegramApplicationContext.mediaManager, account: self.account, postbox: self.account.postbox, playlist: instantPageAudioPlaylist(account: self.account, webpage: webPage, medias: medias, at: media)) - self.account.telegramApplicationContext.mediaManager.setPlaylistPlayer(player) - player.control(.navigation(.next)) + self.account.telegramApplicationContext.mediaManager.setPlaylist(InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex), type: file.isVoice ? .voice : .music) return } @@ -782,7 +788,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let centralIndex = centralIndex { - let controller = InstantPageGalleryController(account: self.account, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in + let controller = InstantPageGalleryController(account: self.account, webPage: webPage, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in }) self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in if let strongSelf = self { diff --git a/TelegramUI/InstantPageGalleryController.swift b/TelegramUI/InstantPageGalleryController.swift index 86d29014dc..fe93d5e2b9 100644 --- a/TelegramUI/InstantPageGalleryController.swift +++ b/TelegramUI/InstantPageGalleryController.swift @@ -26,11 +26,11 @@ struct InstantPageGalleryEntry: Equatable { return lhs.index == rhs.index && lhs.pageId == rhs.pageId && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.location == rhs.location } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings) -> GalleryItem { + func item(account: Account, webPage: TelegramMediaWebpage, theme: PresentationTheme, strings: PresentationStrings) -> GalleryItem { if let image = self.media.media as? TelegramMediaImage { - return InstantImageGalleryItem(account: account, theme: theme, strings: strings, image: image, caption: self.caption, location: self.location) + return InstantImageGalleryItem(account: account, theme: theme, strings: strings, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: self.caption, location: self.location) } else if let file = self.media.media as? TelegramMediaFile, file.isVideo { - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .instantPage(self.pageId, file.fileId), file: file), originData: nil, indexData: GalleryItemIndexData(position: self.location.position, totalCount: self.location.totalCount), contentInfo: nil, caption: self.caption) + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .instantPage(self.pageId, file.fileId), fileReference: .webPage(webPage: WebpageReference(webPage), media: file)), originData: nil, indexData: GalleryItemIndexData(position: self.location.position, totalCount: self.location.totalCount), contentInfo: .webPage(webPage, file), caption: self.caption) } else { preconditionFailure() } @@ -51,6 +51,7 @@ class InstantPageGalleryController: ViewController { } private let account: Account + private let webPage: TelegramMediaWebpage private var presentationData: PresentationData private let _ready = Promise() @@ -77,8 +78,9 @@ class InstantPageGalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void - init(account: Account, entries: [InstantPageGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { + init(account: Account, webPage: TelegramMediaWebpage, entries: [InstantPageGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { self.account = account + self.webPage = webPage self.replaceRootController = replaceRootController self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -98,7 +100,7 @@ class InstantPageGalleryController: ViewController { strongSelf.centralEntryIndex = centralIndex if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings) + $0.item(account: account, webPage: webPage, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings) }), centralItemIndex: centralIndex, keepFirst: false) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in @@ -200,7 +202,7 @@ class InstantPageGalleryController: ViewController { } self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings) + $0.item(account: account, webPage: self.webPage, theme: self.presentationData.theme, strings: self.presentationData.strings) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in diff --git a/TelegramUI/InstantPageImageItem.swift b/TelegramUI/InstantPageImageItem.swift index b01466b2f2..fd85bb955d 100644 --- a/TelegramUI/InstantPageImageItem.swift +++ b/TelegramUI/InstantPageImageItem.swift @@ -6,6 +6,8 @@ import AsyncDisplayKit final class InstantPageImageItem: InstantPageItem { var frame: CGRect + let webPage: TelegramMediaWebpage + let media: InstantPageMedia var medias: [InstantPageMedia] { return [self.media] @@ -17,8 +19,9 @@ final class InstantPageImageItem: InstantPageItem { let wantsNode: Bool = true - init(frame: CGRect, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool) { + init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool) { self.frame = frame + self.webPage = webPage self.media = media self.interactive = interactive self.roundCorners = roundCorners @@ -26,7 +29,7 @@ final class InstantPageImageItem: InstantPageItem { } func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageImageNode(account: account, media: self.media, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia) + return InstantPageImageNode(account: account, webPage: self.webPage, media: self.media, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift index 7047d6d5d7..f51587393a 100644 --- a/TelegramUI/InstantPageImageNode.swift +++ b/TelegramUI/InstantPageImageNode.swift @@ -19,7 +19,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var fetchedDisposable = MetaDisposable() - init(account: Account, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + init(account: Account, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { self.account = account self.media = media self.interactive = interactive @@ -34,11 +34,12 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.addSubnode(self.imageNode) if let image = media.media as? TelegramMediaImage { - self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox - , photo: image)) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) + self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: imageReference).start()) } else if let file = media.media as? TelegramMediaFile { - self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: file)) + let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) + self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, videoReference: fileReference)) } } @@ -73,7 +74,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageSize = largest.dimensions.aspectFilled(size) let boundingSize = size - var radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 + let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 let makeLayout = self.imageNode.asyncLayout() let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 23f335f148..05ac5ee745 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -274,7 +274,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) var items: [InstantPageItem] = [] - let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) contentSize.height += filledSize.height @@ -319,11 +319,11 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo var items: [InstantPageItem] = [] if autoplay { - let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true) + let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true) items.append(mediaItem) } else { - let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - safeInset * 2.0 - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) } @@ -399,7 +399,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo if !author.isEmpty { let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage } if let avatar = avatar { - let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), media: InstantPageMedia(index: -1, media: avatar, caption: ""), interactive: false, roundCorners: true, fit: false) + let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: avatar, caption: ""), interactive: false, roundCorners: true, fit: false) items.append(avatarItem) avatarInset += 62.0 @@ -495,7 +495,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo } } - items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), medias: itemMedias)) + items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), webPage: webpage, medias: itemMedias)) if case .empty = caption { } else { diff --git a/TelegramUI/InstantPageMediaAudioPlaylist.swift b/TelegramUI/InstantPageMediaAudioPlaylist.swift deleted file mode 100644 index 7182df4352..0000000000 --- a/TelegramUI/InstantPageMediaAudioPlaylist.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Foundation -import Postbox -import TelegramCore -import SwiftSignalKit - -struct InstantPageAudioPlaylistItemId: AudioPlaylistItemId { - let index: Int - let id: MediaId - - var hashValue: Int { - return self.id.hashValue &+ self.index.hashValue - } - - func isEqual(to: AudioPlaylistItemId) -> Bool { - if let other = to as? InstantPageAudioPlaylistItemId { - return self.index == other.index && self.id == other.id - } else { - return false - } - } -} - -final class InstantPageAudioPlaylistItem: AudioPlaylistItem { - let media: InstantPageMedia - - var id: AudioPlaylistItemId { - return InstantPageAudioPlaylistItemId(index: self.media.index, id: self.media.media.id!) - } - - var resource: MediaResource? { - if let file = self.media.media as? TelegramMediaFile { - return file.resource - } - return nil - } - - var streamable: Bool { - if let file = self.media.media as? TelegramMediaFile { - if file.isMusic { - return true - } - } - return false - } - - var info: AudioPlaylistItemInfo? { - if let file = self.media.media as? TelegramMediaFile { - for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, duration, title, performer, _): - if isVoice { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) - } else { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) - } - case let .Video(duration, _, flags): - if flags.contains(.instantRoundVideo) { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) - } - default: - break - } - } - return nil - } - return nil - } - - init(media: InstantPageMedia) { - self.media = media - } - - func isEqual(to: AudioPlaylistItem) -> Bool { - if let other = to as? InstantPageAudioPlaylistItem { - return self.media == other.media - } else { - return false - } - } -} - -struct InstantPageAudioPlaylistId: AudioPlaylistId { - let webpageId: MediaId - - func isEqual(to: AudioPlaylistId) -> Bool { - if let other = to as? InstantPageAudioPlaylistId { - if self.webpageId != other.webpageId { - return false - } - return true - } else { - return false - } - } -} - -func instantPageAudioPlaylistAndItemIds(webpage: TelegramMediaWebpage, media: InstantPageMedia) -> (AudioPlaylistId, AudioPlaylistItemId)? { - return (InstantPageAudioPlaylistId(webpageId: webpage.webpageId), InstantPageAudioPlaylistItemId(index: media.index, id: media.media.id!)) -} - -func instantPageAudioPlaylist(account: Account, webpage: TelegramMediaWebpage, medias: [InstantPageMedia], at centralMedia: InstantPageMedia) -> AudioPlaylist { - return AudioPlaylist(id: InstantPageAudioPlaylistId(webpageId: webpage.webpageId), navigate: { item, navigation in - if let item = item as? InstantPageAudioPlaylistItem { - if let index = medias.index(of: item.media) { - switch navigation { - case .previous: - if index == 0 { - return .single(item) - } else { - return .single(InstantPageAudioPlaylistItem(media: medias[index - 1])) - } - case .next: - if index == medias.count - 1 { - return .single(nil) - } else { - return .single(InstantPageAudioPlaylistItem(media: medias[index + 1])) - } - } - } else { - return .single(nil) - } - } else { - if let index = medias.index(of: centralMedia) { - return .single(InstantPageAudioPlaylistItem(media: medias[index])) - } else if let media = medias.first { - return .single(InstantPageAudioPlaylistItem(media: media)) - } else { - return .single(nil) - } - } - }) -} - diff --git a/TelegramUI/InstantPageMediaPlaylist.swift b/TelegramUI/InstantPageMediaPlaylist.swift new file mode 100644 index 0000000000..5675a25dcd --- /dev/null +++ b/TelegramUI/InstantPageMediaPlaylist.swift @@ -0,0 +1,230 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId { + let index: Int + + func isEqual(to: SharedMediaPlaylistItemId) -> Bool { + if let to = to as? InstantPageMediaPlaylistItemId { + if self.index != to.index { + return false + } + return true + } + return false + } +} + +private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? { + return item.media as? TelegramMediaFile +} + +final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { + let webPage: TelegramMediaWebpage + let id: SharedMediaPlaylistItemId + let item: InstantPageMedia + + init(webPage: TelegramMediaWebpage, item: InstantPageMedia) { + self.webPage = webPage + self.id = InstantPageMediaPlaylistItemId(index: item.index) + self.item = item + } + + var stableId: AnyHashable { + return self.item.index + } + + var playbackData: SharedMediaPlaybackData? { + if let file = extractFileMedia(self.item) { + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, _, _, _, _): + if isVoice { + return SharedMediaPlaybackData(type: .voice, source: .telegramFile(.webPage(webPage: WebpageReference(self.webPage), media: file))) + } else { + return SharedMediaPlaybackData(type: .music, source: .telegramFile(.webPage(webPage: WebpageReference(self.webPage), media: file))) + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(.webPage(webPage: WebpageReference(self.webPage), media: file))) + } else { + return nil + } + default: + break + } + } + if file.mimeType.hasPrefix("audio/") { + return SharedMediaPlaybackData(type: .music, source: .telegramFile(.webPage(webPage: WebpageReference(self.webPage), media: file))) + } + if let fileName = file.fileName { + let ext = (fileName as NSString).pathExtension.lowercased() + if ext == "wav" || ext == "opus" { + return SharedMediaPlaybackData(type: .music, source: .telegramFile(.webPage(webPage: WebpageReference(self.webPage), media: file))) + } + } + } + return nil + } + + var displayData: SharedMediaPlaybackDisplayData? { + if let file = extractFileMedia(self.item) { + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return SharedMediaPlaybackDisplayData.voice(author: nil, peer: nil) + } else { + var updatedTitle = title + let updatedPerformer = performer + if (title ?? "").isEmpty && (performer ?? "").isEmpty { + updatedTitle = file.fileName ?? "" + } + return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))) + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + return SharedMediaPlaybackDisplayData.instantVideo(author: nil, peer: nil, timestamp: 0) + } else { + return nil + } + default: + break + } + } + + return SharedMediaPlaybackDisplayData.music(title: file.fileName ?? "", performer: "", albumArt: nil) + } + return nil + } +} + +struct InstantPageMediaPlaylistId: SharedMediaPlaylistId { + let webpageId: MediaId + + func isEqual(to: SharedMediaPlaylistId) -> Bool { + if let to = to as? InstantPageMediaPlaylistId { + return self.webpageId == to.webpageId + } + return false + } +} + +struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation { + let webpageId: MediaId + + func isEqual(to: SharedMediaPlaylistLocation) -> Bool { + guard let to = to as? InstantPagePlaylistLocation else { + return false + } + if self.webpageId == to.webpageId { + return false + } + return true + } +} + +final class InstantPageMediaPlaylist: SharedMediaPlaylist { + private let webPage: TelegramMediaWebpage + private let items: [InstantPageMedia] + private let initialItemIndex: Int + + var location: SharedMediaPlaylistLocation { + return InstantPagePlaylistLocation(webpageId: self.webPage.webpageId) + } + + private var currentItem: InstantPageMedia? + private var playedToEnd: Bool = false + private var order: MusicPlaybackSettingsOrder = .regular + private(set) var looping: MusicPlaybackSettingsLooping = .none + + let id: SharedMediaPlaylistId + + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + + init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) { + assert(Queue.mainQueue().isCurrent()) + + self.id = InstantPageMediaPlaylistId(webpageId: webPage.webpageId) + + self.webPage = webPage + self.items = items + self.initialItemIndex = initialItemIndex + + self.control(.next) + } + + func control(_ action: SharedMediaPlaylistControlAction) { + assert(Queue.mainQueue().isCurrent()) + + switch action { + case .next, .previous: + if let currentItem = self.currentItem, let currentIndex = self.items.index(where: { $0.index == currentItem.index }) { + let selectedIndex: Int? + switch self.order { + case .regular: + if case .next = action { + selectedIndex = max(0, currentIndex - 1) + } else { + if currentIndex == self.items.count - 1 { + selectedIndex = nil + } else { + selectedIndex = currentIndex + 1 + } + } + case .reversed: + if case .next = action { + if currentIndex == self.items.count - 1 { + selectedIndex = nil + } else { + selectedIndex = currentIndex + 1 + } + } else { + selectedIndex = max(0, currentIndex - 1) + } + case .random: + selectedIndex = Int(arc4random_uniform(UInt32(self.items.count))) + } + + if let selectedIndex = selectedIndex { + self.currentItem = self.items[selectedIndex] + self.playedToEnd = false + } else { + self.currentItem = nil + self.playedToEnd = true + } + self.updateState() + } else { + self.currentItem = self.items[self.initialItemIndex] + self.playedToEnd = false + self.updateState() + } + } + } + + func setOrder(_ order: MusicPlaybackSettingsOrder) { + if self.order != order { + self.order = order + self.updateState() + } + } + + func setLooping(_ looping: MusicPlaybackSettingsLooping) { + if self.looping != looping { + self.looping = looping + self.updateState() + } + } + + private func updateState() { + self.stateValue.set(.single(SharedMediaPlaylistState(loading: false, playedToEnd: self.playedToEnd, item: self.currentItem.flatMap({ InstantPageMediaPlaylistItem(webPage: self.webPage, item: $0) }), order: self.order, looping: self.looping))) + } + + func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + } +} diff --git a/TelegramUI/InstantPagePlayableVideoItem.swift b/TelegramUI/InstantPagePlayableVideoItem.swift index 8e7dcdad8c..ce0f42de01 100644 --- a/TelegramUI/InstantPagePlayableVideoItem.swift +++ b/TelegramUI/InstantPagePlayableVideoItem.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit final class InstantPagePlayableVideoItem: InstantPageItem { var frame: CGRect + let webPage: TelegramMediaWebpage let media: InstantPageMedia var medias: [InstantPageMedia] { @@ -15,14 +16,15 @@ final class InstantPagePlayableVideoItem: InstantPageItem { let wantsNode: Bool = true - init(frame: CGRect, media: InstantPageMedia, interactive: Bool) { + init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool) { self.frame = frame + self.webPage = webPage self.media = media self.interactive = interactive } func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { - return InstantPagePlayableVideoNode(account: account, media: self.media, interactive: self.interactive, openMedia: openMedia) + return InstantPagePlayableVideoNode(account: account, webPage: self.webPage, media: self.media, interactive: self.interactive, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift index e09383af67..b67a13544c 100644 --- a/TelegramUI/InstantPagePlayableVideoNode.swift +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -11,8 +11,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { private let interactive: Bool private let openMedia: (InstantPageMedia) -> Void - private let imageNode: TransformImageNode - private let videoNode: ManagedVideoNode + private let videoNode: UniversalVideoNode private var currentSize: CGSize? @@ -20,24 +19,20 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { private var localIsVisible = false - init(account: Account, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + init(account: Account, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { self.account = account self.media = media self.interactive = interactive self.openMedia = openMedia - self.imageNode = TransformImageNode() - self.videoNode = ManagedVideoNode(preferSoftwareDecoding: false, backgroundThread: false) + self.videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), loopVideo: true, enableSound: false, fetchAutomatically: true), priority: .embedded, autoplay: true) super.init() - self.imageNode.contentAnimations = [.firstUpdate] - self.addSubnode(self.imageNode) self.addSubnode(self.videoNode) if let file = media.media as? TelegramMediaFile { - self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: file)) - self.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: file).start()) + self.fetchedDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start()) } } @@ -56,14 +51,8 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { func updateIsVisible(_ isVisible: Bool) { if self.localIsVisible != isVisible { self.localIsVisible = isVisible - - if isVisible { - if let file = media.media as? TelegramMediaFile { - self.videoNode.acquireContext(account: self.account, mediaManager: account.telegramApplicationContext.mediaManager, id: InstantPageManagedMediaId(media: self.media), resource: file.resource, priority: 0) - } - } else { - self.videoNode.discardContext() - } + + self.videoNode.canAttachContent = isVisible } } @@ -78,20 +67,8 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { if self.currentSize != size { self.currentSize = size - self.imageNode.frame = CGRect(origin: CGPoint(), size: size) self.videoNode.frame = CGRect(origin: CGPoint(), size: size) - - if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions { - let imageSize = dimensions.aspectFilled(size) - let boundingSize = size - - let makeLayout = self.imageNode.asyncLayout() - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) - let apply = makeLayout(arguments) - apply() - - self.videoNode.transformArguments = arguments - } + self.videoNode.updateLayout(size: size, transition: .immediate) } } @@ -107,7 +84,6 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode { } func updateHiddenMedia(media: InstantPageMedia?) { - self.imageNode.isHidden = self.media == media self.videoNode.isHidden = self.media == media } diff --git a/TelegramUI/InstantPageSlideshowItem.swift b/TelegramUI/InstantPageSlideshowItem.swift index e67dcce888..1a8f452d59 100644 --- a/TelegramUI/InstantPageSlideshowItem.swift +++ b/TelegramUI/InstantPageSlideshowItem.swift @@ -5,16 +5,18 @@ import AsyncDisplayKit final class InstantPageSlideshowItem: InstantPageItem { var frame: CGRect + let webPage: TelegramMediaWebpage let wantsNode: Bool = true let medias: [InstantPageMedia] - init(frame: CGRect, medias: [InstantPageMedia]) { + init(frame: CGRect, webPage: TelegramMediaWebpage, medias: [InstantPageMedia]) { self.frame = frame + self.webPage = webPage self.medias = medias } func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageSlideshowNode(account: account, medias: self.medias, openMedia: openMedia) + return InstantPageSlideshowNode(account: account, webPage: webPage, medias: self.medias, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift index cff2a03569..58ebd3027f 100644 --- a/TelegramUI/InstantPageSlideshowItemNode.swift +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -60,6 +60,7 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate { private let account: Account + private let webPage: TelegramMediaWebpage private let openMedia: (InstantPageMedia) -> Void private let pageGap: CGFloat @@ -90,8 +91,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe } } - init(account: Account, openMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { + init(account: Account, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { self.account = account + self.webPage = webPage self.openMedia = openMedia self.pageGap = pageGap self.scrollView = UIScrollView() @@ -169,7 +171,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe let media = self.items[index] let contentNode: ASDisplayNode if let _ = media.media as? TelegramMediaImage { - contentNode = InstantPageImageNode(account: self.account, media: media, interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia) + contentNode = InstantPageImageNode(account: self.account, webPage: self.webPage, media: media, interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia) } else if let file = media.media as? TelegramMediaFile { contentNode = ASDisplayNode() } else { @@ -368,10 +370,10 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { private let pagerNode: InstantPageSlideshowPagerNode private let pageControlNode: PageControlNode - init(account: Account, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void) { + init(account: Account, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void) { self.medias = medias - self.pagerNode = InstantPageSlideshowPagerNode(account: account, openMedia: openMedia) + self.pagerNode = InstantPageSlideshowPagerNode(account: account, webPage: webPage, openMedia: openMedia) self.pagerNode.replaceItems(medias, centralItemIndex: nil) self.pageControlNode = PageControlNode(dotColor: .white) diff --git a/TelegramUI/InstantVideoNode.swift b/TelegramUI/InstantVideoNode.swift deleted file mode 100644 index 68502e3d90..0000000000 --- a/TelegramUI/InstantVideoNode.swift +++ /dev/null @@ -1,368 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -private final class SharedInstantVideoContext: SharedVideoContext { - let player: MediaPlayer - let playerNode: MediaPlayerNode - - private let playbackCompletedListeners = Bag<() -> Void>() - - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource) { - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) - var actionAtEndImpl: (() -> Void)? - self.player.actionAtEnd = .loopDisablingSound({ - actionAtEndImpl?() - }) - self.playerNode = MediaPlayerNode(backgroundThread: false) - self.player.attachPlayerNode(self.playerNode) - - super.init() - - actionAtEndImpl = { [weak self] in - if let strongSelf = self { - for listener in strongSelf.playbackCompletedListeners.copyItems() { - listener() - } - } - } - } - - func play() { - assert(Queue.mainQueue().isCurrent()) - self.player.play() - } - - func pause() { - assert(Queue.mainQueue().isCurrent()) - self.player.pause() - } - - func togglePlayPause() { - assert(Queue.mainQueue().isCurrent()) - self.player.togglePlayPause() - } - - func setSoundEnabled(_ value: Bool) { - assert(Queue.mainQueue().isCurrent()) - if value { - self.player.playOnceWithSound(playAndRecord: true) - } else { - self.player.continuePlayingWithoutSound() - } - } - - func setForceAudioToSpeaker(_ value: Bool) { - assert(Queue.mainQueue().isCurrent()) - self.player.setForceAudioToSpeaker(value) - } - - func seek(_ timestamp: Double) { - assert(Queue.mainQueue().isCurrent()) - self.player.seek(timestamp: timestamp) - } - - func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { - return self.playbackCompletedListeners.add(f) - } - - func removePlaybackCompleted(_ index: Int) { - self.playbackCompletedListeners.remove(index) - } -} - -enum InstantVideoNodeSource { - case messageMedia(stableId: AnyHashable, file: TelegramMediaFile) - - fileprivate var id: AnyHashable { - switch self { - case let .messageMedia(stableId, _): - return stableId - } - } - - fileprivate var resource: MediaResource { - switch self { - case let .messageMedia(_, file): - return file.resource - } - } - - fileprivate var file: TelegramMediaFile { - switch self { - case let .messageMedia(_, file): - return file - } - } -} - -private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayInstantVideoShadow")?.precomposed() - -final class InstantVideoNode: OverlayMediaItemNode { - private let manager: MediaManager - private let source: InstantVideoNodeSource - private let priority: Int32 - private let withSound: Bool - private let postbox: Postbox - - private var soundEnabled: Bool - private var forceAudioToSpeaker: Bool - - private var contextId: Int32? - - private var context: SharedInstantVideoContext? - private var contextPlaybackEndedIndex: Int? - private var validLayout: CGSize? - - private var theme: PresentationTheme - - private let backgroundNode: ASImageNode - private let imageNode: TransformImageNode - private var snapshotView: UIView? - private let progressNode: RadialProgressNode - - private var statusDisposable: Disposable? - - var playbackEnded: (() -> Void)? - var tapped: (() -> Void)? - var dismissed: (() -> Void)? - - private var initializedStatus = false - private let _status = Promise() - var status: Signal { - return self._status.get() - } - - override var group: OverlayMediaItemNodeGroup? { - return OverlayMediaItemNodeGroup(rawValue: 0) - } - - override var tempExtendedTopInset: Bool { - return true - } - - init(theme: PresentationTheme, manager: MediaManager, postbox: Postbox, source: InstantVideoNodeSource, priority: Int32, withSound: Bool, forceAudioToSpeaker: Bool) { - self.theme = theme - self.manager = manager - self.source = source - self.priority = priority - self.withSound = withSound - self.forceAudioToSpeaker = forceAudioToSpeaker - self.soundEnabled = withSound - self.postbox = postbox - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.imageNode = TransformImageNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) - - super.init() - - self.backgroundNode.image = backgroundImage - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.imageNode) - - self.imageNode.setSignal(chatMessageVideo(postbox: postbox, video: source.file)) - - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { [weak self] context in - if let strongSelf = self, let context = context as? SharedInstantVideoContext { - context.addPlaybackCompleted { - if let strongSelf = self { - strongSelf.playbackEnded?() - } - } - } - }) - } - - deinit { - if let context = self.context { - if context.playerNode.supernode === self { - context.playerNode.removeFromSupernode() - } - } - - let manager = self.manager - let source = self.source - let contextId = self.contextId - - Queue.mainQueue().async { - if let contextId = contextId { - manager.sharedVideoContextManager.detachSharedVideoContext(id: source.id, index: contextId) - } - } - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - private func updateContext(_ context: SharedInstantVideoContext?) { - assert(Queue.mainQueue().isCurrent()) - - let previous = self.context - self.context = context - if previous !== context { - if let snapshotView = self.snapshotView { - snapshotView.removeFromSuperview() - self.snapshotView = nil - } - if let previous = previous { - if let contextPlaybackEndedIndex = self.contextPlaybackEndedIndex { - previous.removePlaybackCompleted(contextPlaybackEndedIndex) - } - self.contextPlaybackEndedIndex = nil - if let snapshotView = previous.playerNode.view.snapshotView(afterScreenUpdates: false) { - self.snapshotView = snapshotView - snapshotView.frame = self.imageNode.frame - self.view.addSubview(snapshotView) - } - if previous.playerNode.supernode === self { - previous.playerNode.removeFromSupernode() - } - } - if let context = context { - self.contextPlaybackEndedIndex = context.addPlaybackCompleted { [weak self] in - self?.playbackEnded?() - } - if context.playerNode.supernode !== self { - self.addSubnode(context.playerNode) - if let validLayout = self.validLayout { - self.updateLayoutImpl(validLayout) - } - } - } - if self.hasAttachedContext != (context !== nil) { - self.hasAttachedContext = (context !== nil) - self.hasAttachedContextUpdated?(self.hasAttachedContext) - } - } - } - - override func updateLayout(_ size: CGSize) { - if size != self.validLayout { - self.updateLayoutImpl(size) - } - } - - private func updateLayoutImpl(_ size: CGSize) { - self.validLayout = size - - let arguments = TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: CGSize(width: size.width + 2.0, height: size.height + 2.0), boundingSize: size, intrinsicInsets: UIEdgeInsets()) - let videoFrame = CGRect(origin: CGPoint(), size: arguments.boundingSize) - - if let context = self.context { - context.playerNode.transformArguments = arguments - context.playerNode.frame = videoFrame - } - - let backgroundInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) - self.backgroundNode.frame = CGRect(origin: CGPoint(x: -backgroundInsets.left, y: -backgroundInsets.top), size: CGSize(width: videoFrame.size.width + backgroundInsets.left + backgroundInsets.right, height: videoFrame.size.height + backgroundInsets.top + backgroundInsets.bottom)) - - self.imageNode.asyncLayout()(arguments)() - self.imageNode.frame = videoFrame - self.snapshotView?.frame = self.imageNode.frame - } - - func play() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.play() - } - }) - } - - func pause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.pause() - } - }) - } - - func togglePlayPause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.togglePlayPause() - } - }) - } - - func setSoundEnabled(_ value: Bool) { - self.soundEnabled = value - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.setSoundEnabled(value) - } - }) - } - - func setForceAudioToSpeaker(_ value: Bool) { - self.forceAudioToSpeaker = value - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.setForceAudioToSpeaker(value) - } - }) - } - - func seek(_ timestamp: Double) { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - context.seek(timestamp) - } - }) - } - - override func setShouldAcquireContext(_ value: Bool) { - if value { - if self.contextId == nil { - self.contextId = self.manager.sharedVideoContextManager.attachSharedVideoContext(id: source.id, priority: self.priority, create: { - let context = SharedInstantVideoContext(audioSessionManager: manager.audioSession, postbox: self.postbox, resource: self.source.resource) - context.setSoundEnabled(self.soundEnabled) - context.setForceAudioToSpeaker(self.forceAudioToSpeaker) - context.play() - return context - }, update: { [weak self] context in - if let strongSelf = self { - strongSelf.updateContext(context as? SharedInstantVideoContext) - } - }) - } - } else if let contextId = self.contextId { - self.manager.sharedVideoContextManager.detachSharedVideoContext(id: self.source.id, index: contextId) - self.contextId = nil - } - - if !self.initializedStatus { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedInstantVideoContext { - self.initializedStatus = true - self._status.set(context.player.status) - } - }) - } - } - - override func preferredSizeForOverlayDisplay() -> CGSize { - return CGSize(width: 124.0, height: 124.0) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?() - } - } - - override func dismiss() { - self.dismissed?() - } -} diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 40850ae809..0255757387 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -273,8 +273,10 @@ class ItemListControllerNode: ViewControllerTracingNod if let listStyle = self.listStyle { switch listStyle { case .plain: + self.backgroundColor = transition.theme.list.plainBackgroundColor self.listNode.backgroundColor = transition.theme.list.plainBackgroundColor case .blocks: + self.backgroundColor = transition.theme.list.blocksBackgroundColor self.listNode.backgroundColor = transition.theme.list.blocksBackgroundColor } } @@ -286,8 +288,10 @@ class ItemListControllerNode: ViewControllerTracingNod if let _ = self.theme { switch updateStyle { case .plain: + self.backgroundColor = transition.theme.list.plainBackgroundColor self.listNode.backgroundColor = transition.theme.list.plainBackgroundColor case .blocks: + self.backgroundColor = transition.theme.list.blocksBackgroundColor self.listNode.backgroundColor = transition.theme.list.blocksBackgroundColor } } @@ -356,7 +360,10 @@ class ItemListControllerNode: ViewControllerTracingNod self.addSubnode(updatedNode) } } else if let emptyStateNode = self.emptyStateNode { - emptyStateNode.removeFromSupernode() + emptyStateNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak emptyStateNode] _ in + emptyStateNode?.removeFromSupernode() + }) + self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.emptyStateNode = nil } } @@ -384,6 +391,9 @@ class ItemListControllerNode: ViewControllerTracingNod } let updatedTitleContentNode = searchItem.titleContentNode(current: self.navigationBar.contentNode as? (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)) if updatedTitleContentNode !== self.navigationBar.contentNode { + if let titleContentNode = self.navigationBar.contentNode as? ItemListControllerSearchNavigationContentNode { + titleContentNode.deactivate() + } updatedTitleContentNode.setQueryUpdated { [weak self] query in if let strongSelf = self { strongSelf.searchNode?.queryUpdated(query) @@ -400,7 +410,10 @@ class ItemListControllerNode: ViewControllerTracingNod }) } - if let _ = self.navigationBar.contentNode { + if let titleContentNode = self.navigationBar.contentNode { + if let titleContentNode = titleContentNode as? ItemListControllerSearchNavigationContentNode { + titleContentNode.deactivate() + } self.navigationBar.setContentNode(nil, animated: true) } } diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index 4807476266..e3d2e176dd 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -8,20 +8,27 @@ enum ItemListDisclosureStyle { case none } +enum ItemListDisclosureLabelStyle { + case text + case badge +} + class ItemListDisclosureItem: ListViewItem, ItemListItem { let theme: PresentationTheme let icon: UIImage? let title: String let label: String + let labelStyle: ItemListDisclosureLabelStyle let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle let action: (() -> Void)? - init(theme: PresentationTheme, icon: UIImage? = nil, title: String, label: String, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + init(theme: PresentationTheme, icon: UIImage? = nil, title: String, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { self.theme = theme self.icon = icon self.title = title + self.labelStyle = labelStyle self.label = label self.sectionId = sectionId self.style = style @@ -80,6 +87,7 @@ class ItemListDisclosureItemNode: ListViewItemNode { let titleNode: TextNode let labelNode: TextNode let arrowNode: ASImageNode + let labelBadgeNode: ASImageNode private var item: ItemListDisclosureItem? @@ -117,6 +125,11 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.arrowNode.displaysAsynchronously = false self.arrowNode.isLayerBacked = true + self.labelBadgeNode = ASImageNode() + self.labelBadgeNode.displayWithoutProcessing = true + self.labelBadgeNode.displaysAsynchronously = false + self.labelBadgeNode.isLayerBacked = true + self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -133,6 +146,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { let currentItem = self.item + let currentHasBadge = self.labelBadgeNode.image != nil + return { item, params, neighbors in let rightInset: CGFloat switch item.disclosureStyle { @@ -145,9 +160,21 @@ class ItemListDisclosureItemNode: ListViewItemNode { var updateArrowImage: UIImage? var updatedTheme: PresentationTheme? + var updatedLabelBadgeImage: UIImage? + + var hasBadge = false + if case .badge = item.labelStyle { + hasBadge = true + } + if currentItem?.theme !== item.theme { updatedTheme = item.theme updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + if hasBadge { + updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: 20.0, color: item.theme.list.itemAccentColor) + } + } else if hasBadge && !currentHasBadge { + updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: 20.0, color: item.theme.list.itemAccentColor) } var updateIcon = false diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index 4c80d4802d..53f7b019af 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -21,9 +21,10 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let sectionId: ItemListSectionId let action: () -> Void let textUpdated: (String) -> Void + let processPaste: ((String) -> String)? let tag: ItemListItemTag? - init(theme: PresentationTheme, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), spacing: CGFloat = 0.0, clearButton: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), spacing: CGFloat = 0.0, clearButton: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, processPaste: ((String) -> String)? = nil, action: @escaping () -> Void) { self.theme = theme self.title = title self.text = text @@ -34,6 +35,7 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { self.tag = tag self.sectionId = sectionId self.textUpdated = textUpdated + self.processPaste = processPaste self.action = action } @@ -324,4 +326,24 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It self.textNode.textField.becomeFirstResponder() } } + + @objc func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string.count > 1, let item = self.item, let processPaste = item.processPaste { + let result = processPaste(string) + if result != string { + var text = textField.text ?? "" + text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: result) + textField.text = text + if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + result.count) { + let selectionRange = textField.textRange(from: startPosition, to: startPosition) + DispatchQueue.main.async { + textField.selectedTextRange = selectionRange + } + } + self.textFieldTextChanged(textField) + return false + } + } + return true + } } diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 0ad226897a..6ba476522e 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -324,7 +324,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if fileUpdated { if let file = file { updatedImageSignal = chatMessageSticker(account: item.account, file: file, small: false) - updatedFetchSignal = item.account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)) + updatedFetchSignal = fetchedMediaResource(postbox: item.account.postbox, reference: stickerPackFileReference(file).resourceReference(file.resource)) } else { updatedImageSignal = .single({ _ in return nil }) updatedFetchSignal = .complete() diff --git a/TelegramUI/JoinLinkPreviewController.swift b/TelegramUI/JoinLinkPreviewController.swift index 3704e44225..0945d56b16 100644 --- a/TelegramUI/JoinLinkPreviewController.swift +++ b/TelegramUI/JoinLinkPreviewController.swift @@ -51,7 +51,8 @@ public final class JoinLinkPreviewController: ViewController { self?.join() } self.displayNodeDidLoad() - self.disposable.set((joinLinkInformation(self.link, account: self.account) |> deliverOnMainQueue).start(next: { [weak self] result in + self.disposable.set((joinLinkInformation(self.link, account: self.account) + |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { case let .invite(title, photoRepresentation, participantsCount, participants): @@ -60,8 +61,9 @@ public final class JoinLinkPreviewController: ViewController { case let .alreadyJoined(peerId): strongSelf.navigateToPeer(peerId) strongSelf.dismiss() - default: - break + case .invalidHash: + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.GroupInfo_InvitationLinkDoesNotExist, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.dismiss() } } })) diff --git a/TelegramUI/JoinLinkPreviewPeerContentNode.swift b/TelegramUI/JoinLinkPreviewPeerContentNode.swift index cf11b2b65f..9876d60901 100644 --- a/TelegramUI/JoinLinkPreviewPeerContentNode.swift +++ b/TelegramUI/JoinLinkPreviewPeerContentNode.swift @@ -6,6 +6,21 @@ import TelegramCore private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 26.0)! +private final class MoreNode: ASDisplayNode { + private let avatarNode = AvatarNode(font: Font.regular(24.0)) + + init(count: Int) { + super.init() + + self.addSubnode(self.avatarNode) + self.avatarNode.setCustomLetters(["+\(count)"]) + } + + func updateLayout(size: CGSize) { + self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) + } +} + final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? @@ -15,12 +30,14 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer private let peersScrollNode: ASScrollNode private let peerNodes: [SelectablePeerNode] + private let moreNode: MoreNode? init(account: Account, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [Peer], theme: PresentationTheme, strings: PresentationStrings) { self.avatarNode = AvatarNode(font: avatarFont) self.titleNode = ASTextNode() self.countNode = ASTextNode() self.peersScrollNode = ASScrollNode() + self.peersScrollNode.view.showsHorizontalScrollIndicator = false let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor) @@ -31,6 +48,12 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer return node } + if members.count < Int(memberCount) { + self.moreNode = MoreNode(count: Int(memberCount) - members.count) + } else { + self.moreNode = nil + } + super.init() let peer = TelegramGroup(id: PeerId(namespace: 0, id: 0), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], migrationReference: nil, creationDate: 0, version: 0) @@ -56,6 +79,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } self.addSubnode(self.peersScrollNode) } + self.moreNode.flatMap(self.peersScrollNode.addSubnode) } func activate() { @@ -95,7 +119,13 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer peerOffset += peerSize.width } - self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + peerInset * 2.0, height: peerSize.height) + if let moreNode = self.moreNode { + moreNode.updateLayout(size: peerSize) + moreNode.frame = CGRect(origin: CGPoint(x: peerOffset, y: 0.0), size: peerSize) + peerOffset += peerSize.width + } + + self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + (self.moreNode != nil ? peerSize.width : 0.0) + peerInset * 2.0, height: peerSize.height) transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 168.0), size: CGSize(width: size.width, height: peerSize.height))) self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 8fdd5421e4..21652da664 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -6,7 +6,7 @@ import SwiftSignalKit import Postbox import TelegramCore -func legacyAttachmentMenu(account: Account, peer: Peer, editingMessage: Bool, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { +func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: MessageMediaEditingOptions?, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { let controller = TGMenuSheetController(context: parentController.context, dark: false)! controller.dismissesByOutsideTap = true controller.hasSwipeGesture = true @@ -14,41 +14,71 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editingMessage: Bool, sa controller.forceFullScreen = true var itemViews: [Any] = [] - let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: !editingMessage && allowGrouping, allowSelection: !editingMessage, allowEditing: true, document: false)! - carouselItem.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) - carouselItem.recipientName = peer.displayTitle - carouselItem.cameraPressed = { [weak controller] cameraView in - if let controller = controller { - openCamera(cameraView, controller) - } + + var canSendImageOrVideo = false + if let editMediaOptions = editMediaOptions, editMediaOptions.contains(.imageOrVideo) { + canSendImageOrVideo = true + } else { + canSendImageOrVideo = true } - if (peer is TelegramUser || peer is TelegramSecretChat) && peer.id != account.peerId { - carouselItem.hasTimer = true - } - carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles in - if let controller = controller, let carouselItem = carouselItem { - controller.dismiss(animated: true) - let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent - let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos) - sendMessagesWithSignals(signals) + + var carouselItemView: TGAttachmentCarouselItemView? + + var underlyingViews: [UIView] = [] + + if canSendImageOrVideo { + let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: editMediaOptions == nil && allowGrouping, allowSelection: editMediaOptions == nil, allowEditing: true, document: false)! + carouselItemView = carouselItem + carouselItem.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) + carouselItem.recipientName = peer.displayTitle + carouselItem.cameraPressed = { [weak controller] cameraView in + if let controller = controller { + authorizeDeviceAccess(to: .camera, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, present: account.telegramApplicationContext.presentGlobalController, openSettings: account.telegramApplicationContext.applicationBindings.openSettings, { value in + if value { + openCamera(cameraView, controller) + } + }) + } } - }; - carouselItem.allowCaptions = true - itemViews.append(carouselItem) + if (peer is TelegramUser || peer is TelegramSecretChat) && peer.id != account.peerId { + carouselItem.hasTimer = true + } + carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles in + if let controller = controller, let carouselItem = carouselItem { + controller.dismiss(animated: true) + let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent + let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos) + sendMessagesWithSignals(signals) + } + }; + carouselItem.allowCaptions = true + itemViews.append(carouselItem) + + let galleryItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + controller?.dismiss(animated: true) + openGallery() + })! + itemViews.append(galleryItem) + + underlyingViews.append(galleryItem) + } - let galleryItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in - controller?.dismiss(animated: true) - openGallery() - })! - itemViews.append(galleryItem) + var canSendFiles = false + if let editMediaOptions = editMediaOptions, editMediaOptions.contains(.file) { + canSendFiles = true + } else { + canSendFiles = true + } + if canSendFiles { + let fileItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in + controller?.dismiss(animated: true) + openFileGallery() + })! + itemViews.append(fileItem) + underlyingViews.append(fileItem) + } - let fileItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in - controller?.dismiss(animated: true) - openFileGallery() - })! - itemViews.append(fileItem) - - if !editingMessage { + if editMediaOptions == nil { let locationItem = TGMenuSheetButtonItemView(title: strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) openMap() @@ -62,9 +92,9 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editingMessage: Bool, sa itemViews.append(contactItem) } - carouselItem.underlyingViews = [galleryItem, fileItem] + carouselItemView?.underlyingViews = underlyingViews - if !editingMessage { + if editMediaOptions == nil { for i in 0 ..< min(20, recentlyUsedInlineBots.count) { let peer = recentlyUsedInlineBots[i] let addressName = peer.addressName @@ -80,7 +110,7 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editingMessage: Bool, sa } } - carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1) + carouselItemView?.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1) let cancelItem = TGMenuSheetButtonItemView(title: strings.Common_Cancel, type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in controller?.dismiss(animated: true) diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index b545326621..d0c71ad4b3 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -6,7 +6,7 @@ import TelegramCore import Postbox import SwiftSignalKit -func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, saveCapturedPhotos: Bool, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { +func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = .portrait @@ -19,6 +19,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) + controller.inhibitMultipleCapture = editingMedia } else { controller = TGCameraController() } diff --git a/TelegramUI/LegacyComponentsStickers.swift b/TelegramUI/LegacyComponentsStickers.swift index 1df3eddfab..25e96830aa 100644 --- a/TelegramUI/LegacyComponentsStickers.swift +++ b/TelegramUI/LegacyComponentsStickers.swift @@ -140,7 +140,7 @@ final class LegacyStickerImageDataSource: TGImageDataSource { let fitSize = CGSize(width: CGFloat(width), height: CGFloat(height)) - return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: []), small: !highQuality, fitSize: fitSize, completion: { image in + return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), reference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: []), small: !highQuality, fitSize: fitSize, completion: { image in if let image = image { sharedImageCache.setImage(image, forKey: uri, attributes: nil) completion?(TGDataResource(image: image, decoded: true)) diff --git a/TelegramUI/LegacyImageDownloadActor.swift b/TelegramUI/LegacyImageDownloadActor.swift index 548afbf495..dda05fc9a2 100644 --- a/TelegramUI/LegacyImageDownloadActor.swift +++ b/TelegramUI/LegacyImageDownloadActor.swift @@ -66,7 +66,7 @@ final class LegacyImageDownloadActor: ASActor { } } })) - disposables.add(account.postbox.mediaBox.fetchedResource(resource, tag: nil).start()) + disposables.add(account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start()) } } } diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index c911c79689..3e309cc4d9 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -145,7 +145,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), reference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) let attributes: [MessageAttribute] = [] send(.message(text: "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: nil)) } diff --git a/TelegramUI/LegacyLiveUploadInterface.swift b/TelegramUI/LegacyLiveUploadInterface.swift index 0f6f0c83f5..e4fa3b3738 100644 --- a/TelegramUI/LegacyLiveUploadInterface.swift +++ b/TelegramUI/LegacyLiveUploadInterface.swift @@ -36,7 +36,7 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter if let strongSelf = self { if strongSelf.path == nil { strongSelf.path = path - strongSelf.account.messageMediaPreuploadManager.add(network: strongSelf.account.network, postbox: strongSelf.account.postbox, id: strongSelf.id, encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video), source: strongSelf.data.get()) + strongSelf.account.messageMediaPreuploadManager.add(network: strongSelf.account.network, postbox: strongSelf.account.postbox, id: strongSelf.id, encrypt: false, tag: nil, source: strongSelf.data.get()) } strongSelf.size = size diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift index 0801d5e497..62868dd4c2 100644 --- a/TelegramUI/LegacyLocationController.swift +++ b/TelegramUI/LegacyLocationController.swift @@ -145,7 +145,7 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco controller.setLiveLocationsSignal(.single(freezeLocations)) } else { let updatedLocations = SSignal(generator: { subscriber in - let disposable = topPeerActiveLiveLocationMessages(viewTracker: account.viewTracker, peerId: message.id.peerId).start(next: { messages in + let disposable = topPeerActiveLiveLocationMessages(viewTracker: account.viewTracker, accountPeerId: account.peerId, peerId: message.id.peerId).start(next: { (_, messages) in var result: [Any] = [] let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) loop: for message in messages { diff --git a/TelegramUI/LegacyMediaLocations.swift b/TelegramUI/LegacyMediaLocations.swift index fdf9443912..09db8171a9 100644 --- a/TelegramUI/LegacyMediaLocations.swift +++ b/TelegramUI/LegacyMediaLocations.swift @@ -36,7 +36,7 @@ func resourceFromLegacyImageUri(_ uri: String) -> MediaResource? { return nil } - return CloudFileMediaResource(datacenterId: nDatacenterId, volumeId: nVolumeId, localId: nLocalId, secret: nSecret, size: nil) + return CloudFileMediaResource(datacenterId: nDatacenterId, volumeId: nVolumeId, localId: nLocalId, secret: nSecret, size: nil, fileReference: nil) } return nil } diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index eaf1f5643e..117e59a647 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -28,31 +28,38 @@ func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: controller.shouldShowFileTipIfNeeded = showFileTooltip } -func legacyAssetPicker(theme: PresentationTheme, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, NoError> { +func legacyAssetPicker(applicationContext: TelegramApplicationContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, NoError> { return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent + + authorizeDeviceAccess(to: .mediaLibrary(.send), presentationData: presentationData, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in + if !value { + subscriber.putError(NoError()) + return + } - if TGMediaAssetsLibrary.authorizationStatus() == TGMediaLibraryAuthorizationStatusNotDetermined { - TGMediaAssetsLibrary.requestAuthorization(for: TGMediaAssetAnyType, completion: { (status, group) in - if !LegacyComponentsGlobals.provider().accessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) { - subscriber.putError(NoError()) - } else { - Queue.mainQueue().async { - subscriber.putNext({ context in - let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: peer?.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping) - return controller! - }) - subscriber.putCompletion() + if TGMediaAssetsLibrary.authorizationStatus() == TGMediaLibraryAuthorizationStatusNotDetermined { + TGMediaAssetsLibrary.requestAuthorization(for: TGMediaAssetAnyType, completion: { (status, group) in + if !LegacyComponentsGlobals.provider().accessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) { + subscriber.putError(NoError()) + } else { + Queue.mainQueue().async { + subscriber.putNext({ context in + let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: peer?.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping, inhibitSelection: editingMedia) + return controller! + }) + subscriber.putCompletion() + } } - } - }) - } else { - subscriber.putNext({ context in - let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: peer?.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping) - return controller! - }) - subscriber.putCompletion() - } + }) + } else { + subscriber.putNext({ context in + let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: peer?.displayTitle, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping) + return controller! + }) + subscriber.putCompletion() + } + }) return ActionDisposable { @@ -74,7 +81,7 @@ private enum LegacyAssetVideoData { private enum LegacyAssetItem { case image(data: LegacyAssetImageData, caption: String?) case file(data: LegacyAssetImageData, mimeType: String, name: String, caption: String?) - case video(data: LegacyAssetVideoData, previewImage: UIImage?, adjustments: TGVideoEditAdjustments?, caption: String?, asFile: Bool) + case video(data: LegacyAssetVideoData, previewImage: UIImage?, adjustments: TGVideoEditAdjustments?, caption: String?, asFile: Bool, asAnimation: Bool) } private final class LegacyAssetItemWrapper: NSObject { @@ -132,6 +139,16 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [A name = customName } + if let isAnimation = dict["isAnimation"] as? NSNumber, isAnimation.boolValue, mimeType == "video/mp4" { + var result: [AnyHashable: Any] = [:] + + let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! + let duration = (dict["duration"]! as AnyObject).doubleValue! + + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: nil, caption: caption, asFile: false, asAnimation: true), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + return result + } + var result: [AnyHashable: Any] = [:] result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result @@ -144,13 +161,13 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [A if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } } else if (dict["type"] as! NSString) == "cameraVideo" { @@ -165,7 +182,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [A let dimensions = previewImage.pixelSize() let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } } @@ -198,7 +215,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa arc4random_buf(&randomId, 8) let _ = try? heicData.write(to: URL(fileURLWithPath: tempFilePath + ".heic")) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath + ".heic", randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "image/heic", size: nil, attributes: [.FileName(fileName: "image.heic")]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: "image/heic", size: nil, attributes: [.FileName(fileName: "image.heic")]) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) @@ -239,18 +256,18 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) case let .asset(asset): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) default: break } - case let .video(data, previewImage, adjustments, caption, asFile): + case let .video(data, previewImage, adjustments, caption, asFile, asAnimation): var finalDimensions: CGSize var finalDuration: Double switch data { @@ -262,7 +279,9 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa finalDuration = duration } - finalDimensions = TGFitSize(finalDimensions, CGSize(width: 848.0, height: 848.0)) + if !asAnimation { + finalDimensions = TGFitSize(finalDimensions, CGSize(width: 848.0, height: 848.0)) + } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let previewImage = previewImage { @@ -275,7 +294,9 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa } } - finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetCompressedMedium) + if !asAnimation { + finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetCompressedMedium) + } var resourceAdjustments: VideoMediaResourceAdjustments? if let adjustments = adjustments { @@ -297,7 +318,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa } resource = VideoLibraryMediaResource(localIdentifier: asset.backingAsset.localIdentifier, conversion: asFile ? .passthrough : .compress(resourceAdjustments)) case let .tempFile(path, _, _): - if asFile { + if asFile || asAnimation { if let size = fileSize(path) { resource = LocalFileMediaResource(fileId: arc4random64(), size: size) account.postbox.mediaBox.moveResourceData(resource.id, fromTempPath: path) @@ -311,6 +332,9 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: fileName)) + if asAnimation { + fileAttributes.append(.Animated) + } if !asFile { fileAttributes.append(.Video(duration: Int(finalDuration), size: finalDimensions, flags: [])) if let adjustments = adjustments { @@ -320,7 +344,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), reference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: fileAttributes) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) diff --git a/TelegramUI/LegacySecureIdAttachmentMenu.swift b/TelegramUI/LegacySecureIdAttachmentMenu.swift index 262bcbeccd..ff80dd3a58 100644 --- a/TelegramUI/LegacySecureIdAttachmentMenu.swift +++ b/TelegramUI/LegacySecureIdAttachmentMenu.swift @@ -131,7 +131,7 @@ private func recognizedResources(postbox: Postbox, resources: [TelegramMediaReso var signals: [Signal] = [] for resource in resources { let image = Signal { subscriber in - let fetch = postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let fetch = postbox.mediaBox.fetchedResource(resource, parameters: nil).start() let data = (postbox.mediaBox.resourceData(resource) |> map { data -> UIImage? in if data.complete { diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 290653ea71..7a1d9de5c3 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -115,13 +115,13 @@ private struct FetchControls { } private enum FileIconImage: Equatable { - case imageRepresentation(TelegramMediaImageRepresentation) + case imageRepresentation(TelegramMediaFile, TelegramMediaImageRepresentation) case albumArt(SharedMediaPlaybackAlbumArt) static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool { switch lhs { - case let .imageRepresentation(value): - if case .imageRepresentation(value) = rhs { + case let .imageRepresentation(file, value): + if case .imageRepresentation(file, value) = rhs { return true } else { return false @@ -340,7 +340,7 @@ final class ListMessageFileItemNode: ListMessageNode { } if let representation = smallestImageRepresentation(file.previewRepresentations) { - iconImage = .imageRepresentation(representation) + iconImage = .imageRepresentation(file, representation) } let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, timeFormat: .regular) @@ -382,13 +382,12 @@ final class ListMessageFileItemNode: ListMessageNode { if let selectedMedia = selectedMedia { if mediaUpdated { let account = item.account - let messageId = message.id updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: selectedMedia).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: selectedMedia).start()) } }, cancel: { - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: selectedMedia) + messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: selectedMedia) }) } @@ -419,7 +418,7 @@ final class ListMessageFileItemNode: ListMessageNode { var iconImageApply: (() -> Void)? if let iconImage = iconImage { switch iconImage { - case let .imageRepresentation(representation): + case let .imageRepresentation(_, representation): let iconSize = CGSize(width: 42.0, height: 42.0) let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) @@ -435,9 +434,8 @@ final class ListMessageFileItemNode: ListMessageNode { if currentIconImage != iconImage { if let iconImage = iconImage { switch iconImage { - case let .imageRepresentation(representation): - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [representation], reference: nil) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + case let .imageRepresentation(file, representation): + updateIconImageSignal = chatWebpageSnippetFile(account: item.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation) case let .albumArt(albumArt): updateIconImageSignal = playerAlbumArt(postbox: item.account.postbox, albumArt: albumArt, thumbnail: true) diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 9a6a7783ae..38e1f9e4a6 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -19,6 +19,8 @@ final class ListMessageSnippetItemNode: ListMessageNode { private let titleNode: TextNode private let descriptionNode: TextNode + private let instantViewIconNode: ASImageNode + private let linkNode: TextNode private var linkHighlightingNode: LinkHighlightingNode? private let iconTextBackgroundNode: ASImageNode @@ -49,6 +51,13 @@ final class ListMessageSnippetItemNode: ListMessageNode { self.descriptionNode = TextNode() self.descriptionNode.isLayerBacked = true + self.instantViewIconNode = ASImageNode() + self.instantViewIconNode.isLayerBacked = true + self.instantViewIconNode.displaysAsynchronously = false + self.instantViewIconNode.displayWithoutProcessing = true + self.linkNode = TextNode() + self.linkNode.isLayerBacked = true + self.iconTextBackgroundNode = ASImageNode() self.iconTextBackgroundNode.isLayerBacked = true self.iconTextBackgroundNode.displaysAsynchronously = false @@ -65,6 +74,8 @@ final class ListMessageSnippetItemNode: ListMessageNode { self.addSubnode(self.separatorNode) self.addSubnode(self.titleNode) self.addSubnode(self.descriptionNode) + self.addSubnode(self.linkNode) + self.addSubnode(self.instantViewIconNode) self.addSubnode(self.iconImageNode) } @@ -110,12 +121,12 @@ final class ListMessageSnippetItemNode: ListMessageNode { self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) - //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) + let linkNodeMakeLayout = TextNode.asyncLayout(self.linkNode) let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode) let iconImageLayout = self.iconImageNode.asyncLayout() @@ -144,15 +155,18 @@ final class ListMessageSnippetItemNode: ListMessageNode { var title: NSAttributedString? var descriptionText: NSAttributedString? + var linkText: NSAttributedString? var iconText: NSAttributedString? - var iconImageRepresentation: TelegramMediaImageRepresentation? + var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? let applyIconTextBackgroundImage = iconTextBackgroundImage var primaryUrl: String? + var isInstantView = false + var selectedMedia: TelegramMediaWebpage? var processed = false for media in item.message.media { @@ -160,6 +174,10 @@ final class ListMessageSnippetItemNode: ListMessageNode { selectedMedia = webpage if case let .Loaded(content) = webpage.content { + if content.instantPage != nil { + isInstantView = true + } + primaryUrl = content.url processed = true @@ -172,9 +190,13 @@ final class ListMessageSnippetItemNode: ListMessageNode { title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) if let image = content.image { - iconImageRepresentation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 80.0, height: 80.0)) + if let representation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 80.0, height: 80.0)) { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: image), representation) + } } else if let file = content.file { - iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) + if let representation = smallestImageRepresentation(file.previewRepresentations) { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: file), representation) + } } let mutableDescriptionText = NSMutableAttributedString() @@ -186,7 +208,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { let urlString = NSMutableAttributedString() urlString.append(plainUrlString) urlString.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: content.displayUrl, range: NSMakeRange(0, urlString.length)) - mutableDescriptionText.append(urlString) + linkText = urlString descriptionText = mutableDescriptionText } @@ -196,45 +218,65 @@ final class ListMessageSnippetItemNode: ListMessageNode { } if !processed { - loop: for entity in generateTextEntities(item.message.text, enabledTypes: .all) { - switch entity.type { - case .Url, .Email: - var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) - let nsString = item.message.text as NSString - if range.location + range.length > nsString.length { - range.location = max(0, nsString.length - range.length) - range.length = nsString.length - range.location - } - var urlString = nsString.substring(with: range) - var parsedUrl = URL(string: urlString) - if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty { - urlString = "http://" + urlString - parsedUrl = URL(string: urlString) - } - if let url = parsedUrl, let host = url.host { - primaryUrl = urlString - - iconText = NSAttributedString(string: host[.. nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location } - - let urlAttributedString = NSMutableAttributedString() - urlAttributedString.append(NSAttributedString(string: urlString, font: descriptionFont, textColor: item.theme.list.itemAccentColor)) - if item.theme.list.itemAccentColor.isEqual(item.theme.list.itemPrimaryTextColor) { - urlAttributedString.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) + var urlString = nsString.substring(with: range) + var parsedUrl = URL(string: urlString) + if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) } - urlAttributedString.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: urlString, range: NSMakeRange(0, urlAttributedString.length)) - mutableDescriptionText.append(urlAttributedString) + if let url = parsedUrl, let host = url.host { + primaryUrl = urlString + + iconText = NSAttributedString(string: host[.. Void)? - if let iconImageRepresentation = iconImageRepresentation { + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { let iconSize = CGSize(width: 42.0, height: 42.0) let imageCorners = ImageCorners(topLeft: .Corner(2.0), topRight: .Corner(2.0), bottomLeft: .Corner(2.0), bottomRight: .Corner(2.0)) - let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageRepresentation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()) iconImageApply = iconImageLayout(arguments) } - if currentIconImageRepresentation != iconImageRepresentation { - if let iconImageRepresentation = iconImageRepresentation { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 { + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photoReference: imageReference) + } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { + updateIconImageSignal = chatWebpageSnippetFile(account: item.account, fileReference: fileReference, representation: iconImageReferenceAndRepresentation.1) + } } else { updateIconImageSignal = .complete() } } - let contentHeight = 40.0 + descriptionNodeLayout.size.height + let contentHeight = 40.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height var insets = UIEdgeInsets() if dateHeaderAtBottom, let header = item.header { @@ -314,15 +362,25 @@ final class ListMessageSnippetItemNode: ListMessageNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size)) let _ = titleNodeApply() - transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: 32.0), size: descriptionNodeLayout.size)) + let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: 32.0), size: descriptionNodeLayout.size) + transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame) let _ = descriptionNodeApply() + let linkFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: descriptionFrame.maxY), size: linkNodeLayout.size) + transition.updateFrame(node: strongSelf.linkNode, frame: linkFrame) + let _ = linkNodeApply() + + if let image = instantViewImage { + strongSelf.instantViewIconNode.image = image + transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size)) + } + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.0)) transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((42.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((42.0 - iconTextLayout.size.height) / 2.0) + 3.0), size: iconTextLayout.size)) let _ = iconTextApply() - strongSelf.currentIconImageRepresentation = iconImageRepresentation + strongSelf.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1 if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { @@ -447,8 +505,8 @@ final class ListMessageSnippetItemNode: ListMessageNode { } private func urlAtPoint(_ point: CGPoint) -> String? { - let textNodeFrame = self.descriptionNode.frame - if let (_, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.linkNode.frame + if let (_, attributes) = self.linkNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.Url, ] @@ -495,14 +553,14 @@ final class ListMessageSnippetItemNode: ListMessageNode { if let item = self.item { var rects: [CGRect]? if let point = point { - let textNodeFrame = self.descriptionNode.frame - if let (index, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.linkNode.frame + if let (index, attributes) = self.linkNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.Url ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { - rects = self.descriptionNode.attributeRects(name: name, at: index) + rects = self.linkNode.attributeRects(name: name, at: index) break } } @@ -516,9 +574,9 @@ final class ListMessageSnippetItemNode: ListMessageNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.account.peerId) ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.descriptionNode) + self.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode) } - linkHighlightingNode.frame = self.descriptionNode.frame.offsetBy(dx: 0.0, dy: -4.0) + linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0) linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) }) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil diff --git a/TelegramUI/LiveLocationSummaryManager.swift b/TelegramUI/LiveLocationSummaryManager.swift index d88f766bb9..5eb7cb106a 100644 --- a/TelegramUI/LiveLocationSummaryManager.swift +++ b/TelegramUI/LiveLocationSummaryManager.swift @@ -127,18 +127,27 @@ private final class LiveLocationPeerSummaryContext { } func subscribe(_ f: @escaping ([Peer]?) -> Void) -> Disposable { + let wasEmpty = self.subscribers.isEmpty let index = self.subscribers.add({ next in f(next) }) f(self.peers) + if self.subscribers.isEmpty != wasEmpty { + self.updateSubscription() + } + let queue = self.queue return ActionDisposable { [weak self] in queue.async { if let strongSelf = self { + let wasEmpty = strongSelf.subscribers.isEmpty strongSelf.subscribers.remove(index) + if strongSelf.subscribers.isEmpty != wasEmpty { + strongSelf.updateSubscription() + } if strongSelf.isEmpty { strongSelf.becameEmpty() } @@ -148,9 +157,9 @@ private final class LiveLocationPeerSummaryContext { } private func updateSubscription() { - if self.isActive { - self.peerDisposable.set((topPeerActiveLiveLocationMessages(viewTracker: self.viewTracker, peerId: self.peerId) - |> deliverOn(self.queue)).start(next: { [weak self] messages in + if self.isActive || !self.subscribers.isEmpty { + self.peerDisposable.set((topPeerActiveLiveLocationMessages(viewTracker: self.viewTracker, accountPeerId: self.accountPeerId, peerId: self.peerId) + |> deliverOn(self.queue)).start(next: { [weak self] accountPeer, messages in if let strongSelf = self { var peers: [Peer] = [] for message in messages { @@ -160,7 +169,14 @@ private final class LiveLocationPeerSummaryContext { } } } - strongSelf.peers = peers + if let accountPeer = accountPeer, strongSelf.isActive { + peers.append(accountPeer) + } + if peers.isEmpty { + strongSelf.peers = nil + } else { + strongSelf.peers = peers + } } })) } else { diff --git a/TelegramUI/LocationBroadcastActionSheetItem.swift b/TelegramUI/LocationBroadcastActionSheetItem.swift index 931d2dbcb9..903f8804d4 100644 --- a/TelegramUI/LocationBroadcastActionSheetItem.swift +++ b/TelegramUI/LocationBroadcastActionSheetItem.swift @@ -1,15 +1,21 @@ import Foundation import AsyncDisplayKit import Display +import TelegramCore +import Postbox public class LocationBroadcastActionSheetItem: ActionSheetItem { + public let account: Account + public let peer: Peer public let title: String public let beginTimestamp: Double public let timeout: Double public let strings: PresentationStrings public let action: () -> Void - public init(title: String, beginTimestamp: Double, timeout: Double, strings: PresentationStrings, action: @escaping () -> Void) { + public init(account: Account, peer: Peer, title: String, beginTimestamp: Double, timeout: Double, strings: PresentationStrings, action: @escaping () -> Void) { + self.account = account + self.peer = peer self.title = title self.beginTimestamp = beginTimestamp self.timeout = timeout @@ -33,6 +39,8 @@ public class LocationBroadcastActionSheetItem: ActionSheetItem { } } +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 15.0)! + public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme @@ -41,6 +49,7 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { private var item: LocationBroadcastActionSheetItem? private let button: HighlightTrackingButton + private let avatarNode: AvatarNode private let label: ImmediateTextNode private let timerNode: ChatMessageLiveLocationTimerNode @@ -49,6 +58,9 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { self.button = HighlightTrackingButton() + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + self.label = ImmediateTextNode() self.label.isLayerBacked = true self.label.displaysAsynchronously = false @@ -59,6 +71,7 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { super.init(theme: theme) self.view.addSubview(self.button) + self.addSubnode(self.avatarNode) self.addSubnode(self.label) self.addSubnode(self.timerNode) @@ -83,6 +96,8 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { let textColor: UIColor = self.theme.standardActionTextColor self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: textColor) + self.avatarNode.setPeer(account: item.account, peer: item.peer) + self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: item.timeout, strings: item.strings) self.setNeedsLayout() @@ -99,8 +114,13 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { self.button.frame = CGRect(origin: CGPoint(), size: size) - let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) - self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + let avatarInset: CGFloat = 42.0 + let avatarSize: CGFloat = 32.0 + + self.avatarNode.frame = CGRect(origin: CGPoint(x: 16.0, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + + let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - avatarInset - 16.0 - 16.0 - 30.0), height: size.height)) + self.label.frame = CGRect(origin: CGPoint(x: 16.0 + avatarInset, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) let timerSize = CGSize(width: 28.0, height: 28.0) self.timerNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - timerSize.width, y: floorToScreenPixels((size.height - timerSize.height) / 2.0)), size: timerSize) diff --git a/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift b/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift index 97846386b0..d2cb89a4f4 100644 --- a/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift +++ b/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift @@ -13,6 +13,7 @@ enum LocationBroadcastNavigationAccessoryPanelMode { } final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { + private let accountPeerId: PeerId private var theme: PresentationTheme private var strings: PresentationStrings @@ -31,7 +32,8 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { private var validLayout: CGSize? private var peersAndMode: ([Peer], LocationBroadcastNavigationAccessoryPanelMode)? - init(theme: PresentationTheme, strings: PresentationStrings, tapAction: @escaping () -> Void, close: @escaping () -> Void) { + init(accountPeerId: PeerId, theme: PresentationTheme, strings: PresentationStrings, tapAction: @escaping () -> Void, close: @escaping () -> Void) { + self.accountPeerId = accountPeerId self.theme = theme self.strings = strings @@ -85,6 +87,18 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { self.view.addGestureRecognizer(tapRecognizer) } + func updatePresentationData(_ presentationData: PresentationData) { + self.theme = presentationData.theme + self.strings = presentationData.strings + + self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.iconNode.image = PresentationResourcesRootController.navigationLiveLocationIcon(self.theme) + + self.wavesNode.color = self.theme.rootController.navigationBar.accentTextColor + self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayout = size @@ -103,16 +117,24 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { } subtitleString = NSAttributedString(string: text, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) case .peer: - if peers.count == 0 { + let filteredPeers = peers.filter { + $0.id != self.accountPeerId + } + if filteredPeers.count == 0 { subtitleString = NSAttributedString(string: self.strings.Conversation_LiveLocationYou, font: subtitleFont, textColor: self.theme.rootController.navigationBar.accentTextColor) } else { let otherString: String - if peers.count == 1 { + if filteredPeers.count == 1 { otherString = peers[0].compactDisplayTitle } else { otherString = self.strings.Conversation_LiveLocationMembersCount(Int32(peers.count)) } - let rawText = self.strings.Conversation_LiveLocationYouAnd(otherString).0.replacingOccurrences(of: "*", with: "**") + let rawText: String + if filteredPeers.count != peers.count { + rawText = self.strings.Conversation_LiveLocationYouAnd(otherString).0.replacingOccurrences(of: "*", with: "**") + } else { + rawText = otherString + } let body = MarkdownAttributeSet(font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) let accent = MarkdownAttributeSet(font: subtitleFont, textColor: self.theme.rootController.navigationBar.accentTextColor) subtitleString = parseMarkdownIntoAttributedString(rawText, attributes: MarkdownAttributes(body: body, bold: accent, link: body, linkAttribute: { _ in nil })) diff --git a/TelegramUI/LocationBroadcastPanelWavesNode.swift b/TelegramUI/LocationBroadcastPanelWavesNode.swift index 3b2d3a8f71..5a9fbe257e 100644 --- a/TelegramUI/LocationBroadcastPanelWavesNode.swift +++ b/TelegramUI/LocationBroadcastPanelWavesNode.swift @@ -21,7 +21,11 @@ private func degToRad(_ degrees: CGFloat) -> CGFloat { } final class LocationBroadcastPanelWavesNode: ASDisplayNode { - private var color: UIColor + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } private var effectiveProgress: CGFloat = 0.0 { didSet { diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift deleted file mode 100644 index ab19e8b4b0..0000000000 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ /dev/null @@ -1,391 +0,0 @@ -import Foundation -import Postbox -import TelegramCore -import SwiftSignalKit - -enum AudioPlaylistItemLabelInfo: Equatable { - case music(title: String?, performer: String?) - case voice - case video - - static func ==(lhs: AudioPlaylistItemLabelInfo, rhs: AudioPlaylistItemLabelInfo) -> Bool { - switch lhs { - case let .music(lhsTitle, lhsPerformer): - if case let .music(rhsTitle, rhsPerformer) = rhs, lhsTitle == rhsTitle, lhsPerformer == rhsPerformer { - return true - } else { - return false - } - case .voice: - if case .voice = rhs { - return true - } else { - return false - } - case .video: - if case .video = rhs { - return true - } else { - return false - } - } - } -} - -struct AudioPlaylistItemInfo: Equatable { - let duration: Double - let labelInfo: AudioPlaylistItemLabelInfo - - static func ==(lhs: AudioPlaylistItemInfo, rhs: AudioPlaylistItemInfo) -> Bool { - if !lhs.duration.isEqual(to: rhs.duration) { - return false - } - if lhs.labelInfo != rhs.labelInfo { - return false - } - return true - } -} - -protocol AudioPlaylistItemId { - var hashValue: Int { get } - func isEqual(to: AudioPlaylistItemId) -> Bool -} - -protocol AudioPlaylistItem { - var id: AudioPlaylistItemId { get } - var resource: MediaResource? { get } - var info: AudioPlaylistItemInfo? { get } - var streamable: Bool { get } - - func isEqual(to: AudioPlaylistItem) -> Bool -} - -enum AudioPlaylistNavigation { - case next - case previous -} - -enum AudioPlaylistPlayback { - case play - case pause - case togglePlayPause - case seek(Double) -} - -enum AudioPlaylistControl { - case navigation(AudioPlaylistNavigation) - case playback(AudioPlaylistPlayback) - case stop -} - -protocol AudioPlaylistId { - func isEqual(to: AudioPlaylistId) -> Bool -} - -struct AudioPlaylist { - let id: AudioPlaylistId - let navigate: (AudioPlaylistItem?, AudioPlaylistNavigation) -> Signal -} - -struct AudioPlaylistState: Equatable { - let playlistId: AudioPlaylistId - let item: AudioPlaylistItem? - - static func ==(lhs: AudioPlaylistState, rhs: AudioPlaylistState) -> Bool { - if !lhs.playlistId.isEqual(to: rhs.playlistId) { - return false - } - - if let lhsItem = lhs.item, let rhsItem = rhs.item { - if !lhsItem.isEqual(to: rhsItem) { - return false - } - } else if (lhs.item != nil) != (rhs.item != nil) { - return false - } - return true - } -} - -struct AudioPlaylistStateAndStatus: Equatable { - let state: AudioPlaylistState - let playbackId: Int32 - let status: Signal? - - static func ==(lhs: AudioPlaylistStateAndStatus, rhs: AudioPlaylistStateAndStatus) -> Bool { - return lhs.state == rhs.state && lhs.playbackId == rhs.playbackId - } -} - -private enum AudioPlaylistItemPlayer { - case player(MediaPlayer) - case sharedVideo(InstantVideoNode) - - func play() { - switch self { - case let .player(player): - player.play() - case let .sharedVideo(node): - node.play() - } - } - - func pause() { - switch self { - case let .player(player): - player.pause() - case let .sharedVideo(node): - node.pause() - } - } - - func togglePlayPause() { - switch self { - case let .player(player): - player.togglePlayPause() - case let .sharedVideo(node): - node.togglePlayPause() - } - } - - func seek(_ timestamp: Double) { - switch self { - case let .player(player): - player.seek(timestamp: timestamp) - case let .sharedVideo(node): - node.seek(timestamp) - } - } - - func setSoundEnabled(_ value: Bool) { - switch self { - case .player: - break - case let .sharedVideo(node): - node.setSoundEnabled(value) - } - } -} - -private final class AudioPlaylistItemState { - let item: AudioPlaylistItem - let player: AudioPlaylistItemPlayer? - - init(item: AudioPlaylistItem, player: AudioPlaylistItemPlayer?) { - self.item = item - self.player = player - } -} - -private final class AudioPlaylistInternalState { - var currentItem: AudioPlaylistItemState? - let navigationDisposable = MetaDisposable() - var nextPlaybackId: Int32 = 0 -} - -final class ManagedAudioPlaylistPlayer { - private let audioSessionManager: ManagedAudioSession - private let overlayMediaManager: OverlayMediaManager - private weak var account: Account? - private weak var mediaManager: MediaManager? - private let postbox: Postbox - let playlist: AudioPlaylist - - private let currentState = Atomic(value: AudioPlaylistInternalState()) - private let currentStateAndStatusValue = Promise() - private let overlayContextValue = Promise(nil) - - var stateAndStatus: Signal { - return self.currentStateAndStatusValue.get() - } - - var currentVideoNode: InstantVideoNode? - var overlayContextDisposable: Disposable? - - init(audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, mediaManager: MediaManager, account: Account, postbox: Postbox, playlist: AudioPlaylist) { - self.audioSessionManager = audioSessionManager - self.overlayMediaManager = overlayMediaManager - self.mediaManager = mediaManager - self.account = account - self.postbox = postbox - self.playlist = playlist - - self.overlayContextDisposable = (self.overlayContextValue.get() |> deliverOnMainQueue).start(next: { [weak self] node in - if let strongSelf = self { - if strongSelf.currentVideoNode !== node { - if let currentVideoNode = strongSelf.currentVideoNode { - currentVideoNode.setSoundEnabled(false) - strongSelf.overlayMediaManager.controller?.removeNode(currentVideoNode) - } - strongSelf.currentVideoNode = node - if let node = node { - strongSelf.overlayMediaManager.controller?.addNode(node) - } - } - } - }) - } - - deinit { - self.overlayContextDisposable?.dispose() - if let currentVideoNode = self.currentVideoNode { - self.overlayMediaManager.controller?.removeNode(currentVideoNode) - currentVideoNode.setSoundEnabled(false) - } - self.currentState.with { state -> Void in - state.navigationDisposable.dispose() - } - } - - func control(_ control: AudioPlaylistControl) { - switch control { - case let .playback(playback): - self.currentState.with { state -> Void in - if let item = state.currentItem { - switch playback { - case .play: - item.player?.play() - case .pause: - item.player?.pause() - case .togglePlayPause: - item.player?.togglePlayPause() - case let .seek(timestamp): - item.player?.seek(timestamp) - } - } - } - case let .navigation(navigation): - let disposable = MetaDisposable() - var currentItem: AudioPlaylistItem? - self.currentState.with { state -> Void in - state.navigationDisposable.set(disposable) - currentItem = state.currentItem?.item - } - let postbox = self.postbox - let audioSessionManager = self.audioSessionManager - let mediaManager = self.mediaManager - let account = self.account - disposable.set((self.playlist.navigate(currentItem, navigation) - |> deliverOnMainQueue - |> mapToSignal { [weak mediaManager] item -> Signal<(AudioPlaylistItem, AudioPlaylistItemState)?, NoError> in - if let item = item { - var instantVideo: (TelegramMediaFile, MessageId, UInt32)? - if let item = item as? PeerMessageHistoryAudioPlaylistItem { - switch item.entry { - case let .MessageEntry(message, _, _, _): - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isInstantVideo { - instantVideo = (file, message.id, message.stableId) - } - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file { - if file.isInstantVideo { - instantVideo = (file, message.id, message.stableId) - } - } - } - } - if message.flags.contains(.Incoming) { - for attribute in message.attributes { - if let attribute = attribute as? ConsumableContentMessageAttribute { - if !attribute.consumed { - let _ = markMessageContentAsConsumedInteractively(postbox: postbox, messageId: message.id).start() - } - break - } - } - } - case .HoleEntry: - break - } - } - - if let resource = item.resource { - var itemPlayer: AudioPlaylistItemPlayer? - if let instantVideo = instantVideo { - if let mediaManager = mediaManager, let account = account { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let videoNode = InstantVideoNode(theme: presentationData.theme, manager: mediaManager, postbox: account.postbox, source: .messageMedia(stableId: instantVideo.2, file: instantVideo.0), priority: 0, withSound: true, forceAudioToSpeaker: false) - videoNode.tapped = { [weak videoNode] in - videoNode?.togglePlayPause() - } - itemPlayer = .sharedVideo(videoNode) - } - } else { - let player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: resource, streamable: item.streamable, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) - itemPlayer = .player(player) - } - return .single((item, AudioPlaylistItemState(item: item, player: itemPlayer))) - } else { - return .single((item, AudioPlaylistItemState(item: item, player: nil))) - } - } else { - return .single(nil) - } - }).start(next: { [weak self] next in - if let strongSelf = self { - let updatedStateAndStatus = strongSelf.currentState.with { state -> AudioPlaylistStateAndStatus in - if let (item, itemState) = next { - state.currentItem = itemState - var status: Signal? - if let player = itemState.player { - switch player { - case let .player(player): - player.play() - player.actionAtEnd = .action({ - if let strongSelf = self { - strongSelf.control(.navigation(.next)) - } - }) - status = player.status - case let .sharedVideo(videoNode): - videoNode.playbackEnded = { [weak videoNode] in - Queue.mainQueue().async { - if let videoNode = videoNode { - videoNode.setSoundEnabled(false) - videoNode.play() - } - if let strongSelf = self { - strongSelf.control(.navigation(.next)) - } - } - } - videoNode.dismissed = { - if let strongSelf = self { - strongSelf.control(.stop) - } - } - status = videoNode.status - } - } - let playbackId = state.nextPlaybackId - state.nextPlaybackId += 1 - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: item), playbackId: playbackId, status: status) - } else { - state.currentItem = nil - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: nil), playbackId: 0, status: nil) - } - } - strongSelf.currentStateAndStatusValue.set(.single(updatedStateAndStatus)) - var overlayContextValue: InstantVideoNode? - if let (_, itemState) = next { - if let player = itemState.player, case let .sharedVideo(node) = player { - overlayContextValue = node - node.setSoundEnabled(true) - } - } - strongSelf.overlayContextValue.set(.single(overlayContextValue)) - } - })) - case .stop: - let updatedStateAndStatus = self.currentState.with { state -> AudioPlaylistStateAndStatus in - state.currentItem = nil - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: self.playlist.id, item: nil), playbackId: 0, status: nil) - } - self.currentStateAndStatusValue.set(.single(updatedStateAndStatus)) - self.overlayContextValue.set(.single(nil)) - } - } -} diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index bb5faad657..39c68919b5 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -520,7 +520,7 @@ public final class ManagedAudioSession { private func activate() { if let (type, outputMode) = self.currentTypeAndOutputMode { do { - try AVAudioSession.sharedInstance().setActive(true) + try AVAudioSession.sharedInstance().setActive(true, with: [.notifyOthersOnDeactivation]) try self.setupOutputMode(outputMode) diff --git a/TelegramUI/ManagedVideoNode.swift b/TelegramUI/ManagedVideoNode.swift deleted file mode 100644 index 574aea10b1..0000000000 --- a/TelegramUI/ManagedVideoNode.swift +++ /dev/null @@ -1,93 +0,0 @@ -import Foundation -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -class ManagedVideoNode: ASDisplayNode { - private let thumbnailNode: TransformImageNode - - private var videoPlayer: MediaPlayer? - private var playerNode: MediaPlayerNode? - private let videoContextDisposable = MetaDisposable() - var transformArguments: TransformImageArguments? { - didSet { - self.playerNode?.transformArguments = self.transformArguments - } - } - - private let _player = Promise(nil) - var player: Signal { - return self._player.get() - } - - let preferSoftwareDecoding: Bool - let backgroundThread: Bool - - init(preferSoftwareDecoding: Bool = false, backgroundThread: Bool = true) { - self.preferSoftwareDecoding = preferSoftwareDecoding - self.backgroundThread = backgroundThread - - self.thumbnailNode = TransformImageNode() - - super.init() - - self.addSubnode(self.thumbnailNode) - } - - deinit { - self.videoContextDisposable.dispose() - } - - func clearContext() { - self.videoContextDisposable.set(nil) - } - - func acquireContext(account: Account, mediaManager: MediaManager, id: ManagedMediaId, resource: MediaResource, priority: Int32) { - let (player, disposable) = mediaManager.videoContext(postbox: account.postbox, id: id, resource: resource, preferSoftwareDecoding: false, backgroundThread: false, priority: priority, initiatePlayback: true, activate: { [weak self] playerNode in - if let strongSelf = self { - if strongSelf.playerNode !== playerNode { - if strongSelf.playerNode?.supernode === self { - strongSelf.playerNode?.removeFromSupernode() - } - strongSelf.playerNode = playerNode - strongSelf.addSubnode(playerNode) - playerNode.transformArguments = strongSelf.transformArguments - strongSelf.setNeedsLayout() - } - } - }, deactivate: { [weak self] in - if let strongSelf = self { - if let playerNode = strongSelf.playerNode { - strongSelf.playerNode = nil - if playerNode.supernode === strongSelf { - playerNode.removeFromSupernode() - } - } - return .complete() - } else { - return .complete() - } - }) - - self._player.set(.single(player)) - self.videoContextDisposable.set(disposable) - } - - func discardContext() { - self._player.set(.single(nil)) - if let playerNode = self.playerNode { - self.playerNode = nil - if playerNode.supernode === self { - playerNode.removeFromSupernode() - } - } - self.videoContextDisposable.set(nil) - } - - override func layout() { - super.layout() - - self.playerNode?.frame = self.bounds - } -} diff --git a/TelegramUI/MediaInputPaneTrendingItem.swift b/TelegramUI/MediaInputPaneTrendingItem.swift index a1c3d954ac..2a1aa87b50 100644 --- a/TelegramUI/MediaInputPaneTrendingItem.swift +++ b/TelegramUI/MediaInputPaneTrendingItem.swift @@ -268,7 +268,7 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode { if file.fileId != node.file?.fileId { node.file = file node.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) - node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: file).start()) + node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file)).start()) } if let dimensions = file.dimensions { let imageSize = dimensions.aspectFitted(itemSize) diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 74822566bf..5a574519c1 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -8,153 +8,6 @@ import MediaPlayer import TelegramUIPrivateModule -private struct WrappedAudioPlaylistItemId: Hashable, Equatable { - let playlistId: AudioPlaylistId - let itemId: AudioPlaylistItemId - - static func ==(lhs: WrappedAudioPlaylistItemId, rhs: WrappedAudioPlaylistItemId) -> Bool { - return lhs.itemId.isEqual(to: rhs.itemId) && lhs.playlistId.isEqual(to: rhs.playlistId) - } - - var hashValue: Int { - return self.itemId.hashValue - } -} - -struct WrappedManagedMediaId: Hashable { - let id: ManagedMediaId - - var hashValue: Int { - return self.id.hashValue - } - - static func ==(lhs: WrappedManagedMediaId, rhs: WrappedManagedMediaId) -> Bool { - return lhs.id.isEqual(to: rhs.id) - } -} - -final class ManagedVideoContext { - let mediaPlayer: MediaPlayer - let playerNode: MediaPlayerNode? - - init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode?) { - self.mediaPlayer = mediaPlayer - self.playerNode = playerNode - } -} - -final class ManagedVideoContextSubscriber { - let id: Int32 - let priority: Int32 - var active = false - let activate: (MediaPlayerNode) -> Void - let deactivate: () -> Signal - var deactivatingDisposable: Disposable? = nil - - init(id: Int32, priority: Int32, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) { - self.id = id - self.priority = priority - self.activate = activate - self.deactivate = deactivate - } -} - -private final class ActiveManagedVideoContext { - let mediaPlayer: MediaPlayer - let playerNode: MediaPlayerNode - private var becameEmpty: () -> Void - private var nextSubscriberId: Int32 = 0 - var contextSubscribers: [ManagedVideoContextSubscriber] = [] - - init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode, becameEmpty: @escaping () -> Void) { - self.mediaPlayer = mediaPlayer - self.playerNode = playerNode - self.becameEmpty = becameEmpty - } - - func addContextSubscriber(priority: Int32, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) -> Disposable { - let id = self.nextSubscriberId - self.nextSubscriberId += 1 - self.contextSubscribers.append(ManagedVideoContextSubscriber(id: id, priority: priority, activate: activate, deactivate: deactivate)) - self.contextSubscribers.sort(by: { lhs, rhs in - if lhs.priority != rhs.priority { - return lhs.priority < rhs.priority - } else { - return lhs.id < rhs.id - } - }) - self.updateSubscribers() - - return ActionDisposable { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.removeDeactivatedSubscriber(id: id) - } - } - } - } - - private func removeDeactivatedSubscriber(id: Int32) { - assert(Queue.mainQueue().isCurrent()) - - for i in 0 ..< self.contextSubscribers.count { - if self.contextSubscribers[i].id == id { - self.contextSubscribers[i].deactivatingDisposable?.dispose() - self.contextSubscribers.remove(at: i) - self.updateSubscribers() - break - } - } - } - - private func updateSubscribers() { - assert(Queue.mainQueue().isCurrent()) - - if !self.contextSubscribers.isEmpty { - var activeIndex: Int? - var deactivating = false - var index = 0 - for subscriber in self.contextSubscribers { - if subscriber.active { - activeIndex = index - break - } - else if subscriber.deactivatingDisposable != nil { - deactivating = false - } - index += 1 - } - if !deactivating { - if let activeIndex = activeIndex, activeIndex != self.contextSubscribers.count - 1 { - self.contextSubscribers[activeIndex].active = false - let id = self.contextSubscribers[activeIndex].id - self.contextSubscribers[activeIndex].deactivatingDisposable = (self.contextSubscribers[activeIndex].deactivate() |> deliverOn(Queue.mainQueue())).start(completed: { [weak self] in - if let strongSelf = self { - var index = 0 - for currentRecord in strongSelf.contextSubscribers { - if currentRecord.id == id { - currentRecord.deactivatingDisposable = nil - break - } - index += 1 - } - strongSelf.updateSubscribers() - } - }) - } else if activeIndex == nil { - let lastIndex = self.contextSubscribers.count - 1 - self.contextSubscribers[lastIndex].active = true - //self.applyType(self.contextSubscribers[lastIndex].audioSessionType) - - self.contextSubscribers[lastIndex].activate(self.playerNode) - } - } - } else { - self.becameEmpty() - } - } -} - enum SharedMediaPlayerGroup: Int { case music = 0 case voiceAndInstantVideo = 1 @@ -173,6 +26,21 @@ private let sharedAudioSession: ManagedAudioSession = { return audioSession }() +private struct GlobalControlOptions: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32 = 0) { + self.rawValue = rawValue + } + + static let play = GlobalControlOptions(rawValue: 1 << 0) + static let pause = GlobalControlOptions(rawValue: 1 << 1) + static let previous = GlobalControlOptions(rawValue: 1 << 2) + static let next = GlobalControlOptions(rawValue: 1 << 3) + static let playPause = GlobalControlOptions(rawValue: 1 << 4) + static let seek = GlobalControlOptions(rawValue: 1 << 5) +} + public final class MediaManager: NSObject { public static var globalAudioSession: ManagedAudioSession { return sharedAudioSession @@ -240,13 +108,6 @@ public final class MediaManager: NSObject { private let setPlaylistByTypeDisposables = DisposableDict() - private let playlistPlayer = Atomic(value: nil) - private let playlistPlayerStateAndStatusValue = Promise(nil) - var playlistPlayerStateAndStatus: Signal { - return self.playlistPlayerStateAndStatusValue.get() - } - private let playlistPlayerStateValueDisposable = MetaDisposable() - private let sharedPlayerByGroup: [SharedMediaPlayerGroup: SharedMediaPlayer] = [:] private var currentOverlayVideoNode: OverlayMediaItemNode? @@ -258,8 +119,6 @@ public final class MediaManager: NSObject { private let globalControlsStatusDisposable = MetaDisposable() private let globalAudioSessionForegroundDisposable = MetaDisposable() - private var managedVideoContexts: [WrappedManagedMediaId: ActiveManagedVideoContext] = [:] - let universalVideoManager = UniversalVideoContentManager() let galleryHiddenMediaManager = GalleryHiddenMediaManager() @@ -286,17 +145,35 @@ public final class MediaManager: NSObject { })) let commandCenter = MPRemoteCommandCenter.shared() + + commandCenter.playCommand.isEnabled = false commandCenter.playCommand.addTarget(self, action: #selector(playCommandEvent(_:))) + + commandCenter.pauseCommand.isEnabled = false commandCenter.pauseCommand.addTarget(self, action: #selector(pauseCommandEvent(_:))) + + commandCenter.previousTrackCommand.isEnabled = false commandCenter.previousTrackCommand.addTarget(self, action: #selector(previousTrackCommandEvent(_:))) + + commandCenter.nextTrackCommand.isEnabled = false commandCenter.nextTrackCommand.addTarget(self, action: #selector(nextTrackCommandEvent(_:))) + + commandCenter.togglePlayPauseCommand.isEnabled = false commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(togglePlayPauseCommandEvent(_:))) + + var baseNowPlayingInfo: [String: Any]? + if #available(iOSApplicationExtension 9.1, *) { + commandCenter.changePlaybackPositionCommand.isEnabled = false commandCenter.changePlaybackPositionCommand.addTarget(handler: { [weak self] event in if let strongSelf = self, let event = event as? MPChangePlaybackPositionCommandEvent { strongSelf.playlistControl(.seek(event.positionTime)) } - return .success + if baseNowPlayingInfo != nil { + return .success + } else { + return .noActionableNowPlayingItem + } }) } @@ -305,19 +182,41 @@ public final class MediaManager: NSObject { let globalControlsArtwork = self.globalControlsArtwork let globalControlsStatus = self.globalControlsStatus - var baseNowPlayingInfo: [String: Any]? + var currentGlobalControlsOptions = GlobalControlOptions() - self.globalControlsDisposable.set((self.globalMediaPlayerState |> deliverOnMainQueue).start(next: { stateAndType in + self.globalControlsDisposable.set((self.globalMediaPlayerState + |> deliverOnMainQueue).start(next: { stateAndType in + var updatedGlobalControlOptions = GlobalControlOptions() + if let (state, type) = stateAndType { + if type == .music { + updatedGlobalControlOptions.insert(.previous) + updatedGlobalControlOptions.insert(.next) + updatedGlobalControlOptions.insert(.seek) + switch state.status.status { + case .playing, .buffering(_, true): + updatedGlobalControlOptions.insert(.pause) + default: + updatedGlobalControlOptions.insert(.play) + } + } + } + + if currentGlobalControlsOptions != updatedGlobalControlOptions { + currentGlobalControlsOptions = updatedGlobalControlOptions + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = updatedGlobalControlOptions.contains(.play) + commandCenter.pauseCommand.isEnabled = updatedGlobalControlOptions.contains(.pause) + commandCenter.previousTrackCommand.isEnabled = updatedGlobalControlOptions.contains(.previous) + commandCenter.nextTrackCommand.isEnabled = updatedGlobalControlOptions.contains(.next) + commandCenter.togglePlayPauseCommand.isEnabled = !updatedGlobalControlOptions.intersection([.play, .pause]).isEmpty + if #available(iOSApplicationExtension 9.1, *) { + commandCenter.changePlaybackPositionCommand.isEnabled = updatedGlobalControlOptions.contains(.seek) + } + } + if let (state, type) = stateAndType, type == .music, let displayData = state.item.displayData { if previousDisplayData != displayData { previousDisplayData = displayData - - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.playCommand.isEnabled = true - commandCenter.pauseCommand.isEnabled = true - commandCenter.previousTrackCommand.isEnabled = true - commandCenter.nextTrackCommand.isEnabled = true - commandCenter.togglePlayPauseCommand.isEnabled = true var nowPlayingInfo: [String: Any] = [:] @@ -336,7 +235,7 @@ public final class MediaManager: NSObject { let titleText: String = author?.displayTitle ?? "" nowPlayingInfo[MPMediaItemPropertyTitle] = titleText - case let .instantVideo(author, _): + case let .instantVideo(author, _, _): let titleText: String = author?.displayTitle ?? "" nowPlayingInfo[MPMediaItemPropertyTitle] = titleText @@ -356,18 +255,20 @@ public final class MediaManager: NSObject { } else { previousState = nil previousDisplayData = nil - baseNowPlayingInfo = nil globalControlsStatus.set(.single(nil)) globalControlsArtwork.set(.single(nil)) - let commandCenter = MPRemoteCommandCenter.shared() + /*let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = false commandCenter.pauseCommand.isEnabled = false commandCenter.previousTrackCommand.isEnabled = false commandCenter.nextTrackCommand.isEnabled = false - commandCenter.togglePlayPauseCommand.isEnabled = false + commandCenter.togglePlayPauseCommand.isEnabled = false*/ - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + if baseNowPlayingInfo != nil { + baseNowPlayingInfo = nil + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } } })) @@ -376,7 +277,7 @@ public final class MediaManager: NSObject { |> mapToSignal { value -> Signal in if let value = value { return Signal { subscriber in - let fetched = postbox.mediaBox.fetchedResource(value.fullSizeResource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start() + let fetched = postbox.mediaBox.fetchedResource(value.fullSizeResource, parameters: nil).start() let data = postbox.mediaBox.resourceData(value.fullSizeResource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)).start(next: { data in if data.complete, let value = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { subscriber.putNext(UIImage(data: value)) @@ -409,7 +310,8 @@ public final class MediaManager: NSObject { } })) - self.globalControlsStatusDisposable.set((self.globalControlsStatus.get() |> deliverOnMainQueue).start(next: { next in + self.globalControlsStatusDisposable.set((self.globalControlsStatus.get() + |> deliverOnMainQueue).start(next: { next in if let next = next { if var nowPlayingInfo = baseNowPlayingInfo { nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = next.duration as NSNumber @@ -423,14 +325,7 @@ public final class MediaManager: NSObject { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - }/* else { - if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo { - nowPlayingInfo.removeValue(forKey: MPMediaItemPropertyPlaybackDuration) - nowPlayingInfo.removeValue(forKey: MPNowPlayingInfoPropertyPlaybackRate) - nowPlayingInfo.removeValue(forKey: MPNowPlayingInfoPropertyElapsedPlaybackTime) - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - } - }*/ + } })) @@ -471,7 +366,6 @@ public final class MediaManager: NSObject { } deinit { - self.playlistPlayerStateValueDisposable.dispose() self.globalControlsDisposable.dispose() self.globalControlsArtworkDisposable.dispose() self.globalControlsStatusDisposable.dispose() @@ -479,39 +373,6 @@ public final class MediaManager: NSObject { self.globalAudioSessionForegroundDisposable.dispose() } - func videoContext(postbox: Postbox, id: ManagedMediaId, resource: MediaResource, preferSoftwareDecoding: Bool, backgroundThread: Bool, priority: Int32, initiatePlayback: Bool, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) -> (MediaPlayer, Disposable) { - assert(Queue.mainQueue().isCurrent()) - - let wrappedId = WrappedManagedMediaId(id: id) - let activeContext: ActiveManagedVideoContext - var startPlayback = false - if let currentActiveContext = self.managedVideoContexts[wrappedId] { - activeContext = currentActiveContext - } else { - let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false, fetchAutomatically: true) - mediaPlayer.actionAtEnd = .loop(nil) - let playerNode = MediaPlayerNode(backgroundThread: backgroundThread) - mediaPlayer.attachPlayerNode(playerNode) - - activeContext = ActiveManagedVideoContext(mediaPlayer: mediaPlayer, playerNode: playerNode, becameEmpty: { [weak self] in - if let strongSelf = self { - strongSelf.managedVideoContexts[wrappedId]?.playerNode.removeFromSupernode() - strongSelf.managedVideoContexts.removeValue(forKey: wrappedId) - } - }) - self.managedVideoContexts[wrappedId] = activeContext - if initiatePlayback { - startPlayback = true - } - } - - if startPlayback { - activeContext.mediaPlayer.play() - } - - return (activeContext.mediaPlayer, activeContext.addContextSubscriber(priority: priority, activate: activate, deactivate: deactivate)) - } - func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -595,54 +456,6 @@ public final class MediaManager: NSObject { } } - func setPlaylistPlayer(_ player: ManagedAudioPlaylistPlayer?) { - var disposePlayer: ManagedAudioPlaylistPlayer? - var updatedPlayer = false - let _ = self.playlistPlayer.modify { currentPlayer in - if currentPlayer !== player { - disposePlayer = currentPlayer - updatedPlayer = true - return player - } else { - return currentPlayer - } - } - - if let disposePlayer = disposePlayer { - withExtendedLifetime(disposePlayer, { - - }) - } - - if updatedPlayer { - if let player = player { - self.playlistPlayerStateAndStatusValue.set(player.stateAndStatus) - self.playlistPlayerStateValueDisposable.set(player.stateAndStatus.start(next: { [weak self] next in - if let next = next { - if next.state.item == nil { - Queue.mainQueue().async { - self?.setPlaylistPlayer(nil) - } - } - } - })) - } else { - self.playlistPlayerStateAndStatusValue.set(.single(nil)) - } - } - } - - func playlistPlayerControl(_ control: AudioPlaylistControl) { - var player: ManagedAudioPlaylistPlayer? - self.playlistPlayer.with { currentPlayer -> Void in - player = currentPlayer - } - - if let player = player { - player.control(control) - } - } - func filteredPlaylistState(playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal { let signal: Signal switch type { @@ -663,16 +476,6 @@ public final class MediaManager: NSObject { }) } - func filteredPlaylistPlayerStateAndStatus(playlistId: AudioPlaylistId, itemId: AudioPlaylistItemId) -> Signal { - return self.playlistPlayerStateAndStatusValue.get() - |> map { state -> AudioPlaylistStateAndStatus? in - if let state = state, let item = state.state.item, state.state.playlistId.isEqual(to: playlistId), item.id.isEqual(to: itemId) { - return state - } - return nil - } - } - @objc func playCommandEvent(_ command: AnyObject) { self.playlistControl(.playback(.play)) } diff --git a/TelegramUI/MediaNavigationAccessoryContainerNode.swift b/TelegramUI/MediaNavigationAccessoryContainerNode.swift index 9598c94f29..80c30d7385 100644 --- a/TelegramUI/MediaNavigationAccessoryContainerNode.swift +++ b/TelegramUI/MediaNavigationAccessoryContainerNode.swift @@ -31,6 +31,13 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog } } + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.headerNode.updatePresentationData(presentationData) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.currentHeaderHeight))) diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index 3d04485d3d..5bb5c6834f 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -151,6 +151,17 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.view.addGestureRecognizer(tapRecognizer) } + func updatePresentationData(_ presentationData: PresentationData) { + self.theme = presentationData.theme + self.strings = presentationData.strings + + self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) + self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) + self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme) + self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor + self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor)) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight @@ -179,9 +190,9 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) - case let .instantVideo(author, peer): + case let .instantVideo(author, peer, timestamp): let titleText: String = author?.displayTitle ?? "" - let subtitleText: String + var subtitleText: String if let peer = peer { if peer is TelegramGroup || peer is TelegramChannel { @@ -193,6 +204,10 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { subtitleText = self.strings.MusicPlayer_VoiceNote } + if titleText == subtitleText { + subtitleText = humanReadableStringForTimestamp(strings: self.strings, timeFormat: .military, timestamp: timestamp) + } + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) } diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift deleted file mode 100644 index 6e1b22a936..0000000000 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ /dev/null @@ -1,206 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import TelegramCore -import Postbox - -final class MediaNavigationAccessoryItemListNode: ASDisplayNode { - static let minimizedPanelHeight: CGFloat = 31.0 - - private var theme: PresentationTheme - - var collapse: (() -> Void)? - - private var previousMaximizedHeight: CGFloat? - - private let account: Account - - private let topSeparatorNode: ASDisplayNode - private let bottomSeparatorNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let panelNode: HighlightTrackingButtonNode - private let panelHandleNode: ASImageNode - private let contentNode: ASDisplayNode - private var listNode: ChatHistoryListNode? - - var stateAndStatus: AudioPlaylistStateAndStatus? { - didSet { - if self.stateAndStatus != oldValue { - let previousPlaylistPeerId = (oldValue?.state.playlistId as? PeerMessageHistoryAudioPlaylistId)?.peerId - let updatedPlaylistPeerId = (self.stateAndStatus?.state.playlistId as? PeerMessageHistoryAudioPlaylistId)?.peerId - - if previousPlaylistPeerId != updatedPlaylistPeerId { - if let listNode = self.listNode { - listNode.removeFromSupernode() - self.listNode = nil - } - if let updatedPlaylistPeerId = updatedPlaylistPeerId { - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in - if let strongSelf = self, let listNode = strongSelf.listNode { - var galleryMedia: Media? - if let message = listNode.messageInCurrentHistoryView(message.id) { - for media in message.media { - if let file = media as? TelegramMediaFile { - galleryMedia = file - } else if let image = media as? TelegramMediaImage { - galleryMedia = image - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file { - galleryMedia = file - } else if let image = content.image { - galleryMedia = image - } - } - } - } - - if let galleryMedia = galleryMedia { - if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, account: strongSelf.account, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: message.id)) - applicationContext.mediaManager.setPlaylistPlayer(player) - player.control(.navigation(.next)) - } - } - } - } - return false - }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) - - let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) - listNode.preloadPages = true - self.listNode = listNode - self.contentNode.addSubnode(listNode) - - if let previousMaximizedHeight = self.previousMaximizedHeight { - self.updateLayout(size: self.bounds.size, maximizedHeight: previousMaximizedHeight, transition: .immediate) - } - } - } else { - let previousPlaylistMessageId = (oldValue?.state.item?.id as? PeerMessageHistoryAudioPlaylistItemId)?.id - let updatedPlaylistMessageId = (self.stateAndStatus?.state.item?.id as? PeerMessageHistoryAudioPlaylistItemId)?.id - if let updatedPlaylistMessageId = updatedPlaylistMessageId, previousPlaylistMessageId != updatedPlaylistMessageId { - if let listNode = self.listNode { - var foundItemNode: ListMessageFileItemNode? - listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ListMessageFileItemNode, let message = itemNode.message, message.id == updatedPlaylistMessageId { - foundItemNode = itemNode - } - } - if let foundItemNode = foundItemNode { - listNode.ensureItemNodeVisible(foundItemNode) - } else if let message = listNode.messageInCurrentHistoryView(updatedPlaylistMessageId) { - listNode.scrollToMessage(from: MessageIndex(message), to: MessageIndex(message), animated: true) - } - } - } - } - } - } - } - - init(account: Account) { - self.account = account - - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.theme = presentationData.theme - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.isLayerBacked = true - self.topSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - - self.bottomSeparatorNode = ASDisplayNode() - self.bottomSeparatorNode.isLayerBacked = true - self.bottomSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - - self.panelNode = HighlightTrackingButtonNode() - self.panelNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor - - self.panelHandleNode = ASImageNode() - self.panelHandleNode.displaysAsynchronously = false - self.panelHandleNode.displayWithoutProcessing = true - self.panelHandleNode.image = PresentationResourcesRootController.navigationPlayerHandleIcon(self.theme) - - self.contentNode = ASDisplayNode() - self.contentNode.backgroundColor = self.theme.chatList.backgroundColor - self.contentNode.clipsToBounds = true - - super.init() - - self.addSubnode(self.contentNode) - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.panelNode) - self.panelNode.addSubnode(self.panelHandleNode) - self.addSubnode(self.bottomSeparatorNode) - self.addSubnode(self.separatorNode) - - self.panelNode.addTarget(self, action: #selector(self.panelPressed), forControlEvents: .touchUpInside) - self.panelNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - //strongSelf.panelNode.layer.removeAnimation(forKey: "opacity") - //strongSelf.panelNode.alpha = 0.55 - } else { - //strongSelf.panelNode.alpha = 0.35 - //strongSelf.panelNode.layer.animateAlpha(from: 0.55, to: 0.35, duration: 0.2) - } - } - } - } - - func updateLayout(size: CGSize, maximizedHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.previousMaximizedHeight = maximizedHeight - - let separatorAlpha: CGFloat = size.height.isLessThanOrEqualTo(MediaNavigationAccessoryItemListNode.minimizedPanelHeight) ? 0.0 : 1.0 - transition.updateAlpha(node: self.separatorNode, alpha: separatorAlpha) - transition.updateAlpha(node: self.panelHandleNode, alpha: min(1.0, max(0.0, size.height / MediaNavigationAccessoryItemListNode.minimizedPanelHeight))) - - transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.panelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - MediaNavigationAccessoryItemListNode.minimizedPanelHeight), size: CGSize(width: size.width, height: MediaNavigationAccessoryItemListNode.minimizedPanelHeight))) - transition.updateFrame(node: self.panelHandleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - 36.0) / 2.0), y: (size.height - 19.0) - (size.height - MediaNavigationAccessoryItemListNode.minimizedPanelHeight)), size: CGSize(width: 36.0, height: 7.0))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - MediaNavigationAccessoryItemListNode.minimizedPanelHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: max(0.0, size.height - MediaNavigationAccessoryItemListNode.minimizedPanelHeight)))) - - if let listNode = listNode { - let listNodeSize = CGSize(width: size.width, height: max(10.0, maximizedHeight - MediaNavigationAccessoryItemListNode.minimizedPanelHeight)) - listNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listNodeSize) - - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: UIEdgeInsets(top: 0.0, left: - 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: listViewCurve) - listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) - } - //transition.updateFrame(node: self.contentNode, frame: )) - } - - @objc func panelPressed() { - self.collapse?() - } -} diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index f37a2607ed..29034cd13d 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -58,7 +58,7 @@ private final class MediaPlayerContext { private let audioSessionManager: ManagedAudioSession private let postbox: Postbox - private let resource: MediaResource + private let resourceReference: MediaResourceReference private let streamable: Bool private let video: Bool private let preferSoftwareDecoding: Bool @@ -83,14 +83,14 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool) { assert(queue.isCurrent()) self.queue = queue self.audioSessionManager = audioSessionManager self.playerStatus = playerStatus self.postbox = postbox - self.resource = resource + self.resourceReference = resourceReference self.streamable = streamable self.video = video self.preferSoftwareDecoding = preferSoftwareDecoding @@ -237,7 +237,7 @@ private final class MediaPlayerContext { self.playerStatus.set(status) } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resource: self.resource, streamable: self.streamable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resourceReference: self.resourceReference, streamable: self.streamable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically) let disposable = MetaDisposable() self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action, enableSound: self.enableSound) @@ -817,9 +817,9 @@ final class MediaPlayer { } } - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = true) { + init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = true) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resourceReference: resourceReference, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index 6285ee3e2f..9af34f66c8 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -167,7 +167,7 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode { } final class MediaPlayerScrubbingNode: ASDisplayNode { - private let contentNodes: MediaPlayerScrubbingNodeContentNodes + private var contentNodes: MediaPlayerScrubbingNodeContentNodes private var playbackStatusValue: MediaPlayerPlaybackStatus? private var scrubbingBeginTimestamp: Double? @@ -241,183 +241,205 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } } - init(content: MediaPlayerScrubbingNodeContent) { + private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes { switch content { - case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor): - let backgroundNode = ASImageNode() - backgroundNode.isLayerBacked = true - backgroundNode.displaysAsynchronously = false - backgroundNode.displayWithoutProcessing = true - - let bufferingNode = MediaPlayerScrubbingBufferingNode(color: foregroundColor.withAlphaComponent(0.5), lineCap: lineCap, lineHeight: lineHeight) - - let foregroundContentNode = ASImageNode() - foregroundContentNode.isLayerBacked = true - foregroundContentNode.displaysAsynchronously = false - foregroundContentNode.displayWithoutProcessing = true - - switch lineCap { - case .round: - backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) - foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) - case .square: - backgroundNode.backgroundColor = backgroundColor - foregroundContentNode.backgroundColor = foregroundColor - } - - let foregroundNode = MediaPlayerScrubbingForegroundNode() - foregroundNode.isLayerBacked = true - foregroundNode.clipsToBounds = true - - var handleNodeImpl: ASImageNode? - var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton? - - switch scrubberHandle { - case .none: - break - case .line: - let handleNode = ASImageNode() - handleNode.image = generateHandleBackground(color: foregroundColor) - handleNode.isLayerBacked = true - handleNodeImpl = handleNode - - let handleNodeContainer = MediaPlayerScrubbingNodeButton() - handleNodeContainer.addSubnode(handleNode) - handleNodeContainerImpl = handleNodeContainer - case .circle: - let handleNode = ASImageNode() - handleNode.image = generateFilledCircleImage(diameter: 7.0, color: foregroundColor) - handleNode.isLayerBacked = true - handleNodeImpl = handleNode - - let handleNodeContainer = MediaPlayerScrubbingNodeButton() - handleNodeContainer.addSubnode(handleNode) - handleNodeContainerImpl = handleNodeContainer - } - - handleNodeContainerImpl?.isUserInteractionEnabled = self.enableScrubbing - - self.contentNodes = .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNode: handleNodeImpl, handleNodeContainer: handleNodeContainerImpl)) - case let .custom(backgroundNode, foregroundContentNode): - let foregroundNode = MediaPlayerScrubbingForegroundNode() - foregroundNode.isLayerBacked = true - foregroundNode.clipsToBounds = true + case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor): + let backgroundNode = ASImageNode() + backgroundNode.isLayerBacked = true + backgroundNode.displaysAsynchronously = false + backgroundNode.displayWithoutProcessing = true + + let bufferingNode = MediaPlayerScrubbingBufferingNode(color: foregroundColor.withAlphaComponent(0.5), lineCap: lineCap, lineHeight: lineHeight) + + let foregroundContentNode = ASImageNode() + foregroundContentNode.isLayerBacked = true + foregroundContentNode.displaysAsynchronously = false + foregroundContentNode.displayWithoutProcessing = true + + switch lineCap { + case .round: + backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) + foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + case .square: + backgroundNode.backgroundColor = backgroundColor + foregroundContentNode.backgroundColor = foregroundColor + } + + let foregroundNode = MediaPlayerScrubbingForegroundNode() + foregroundNode.isLayerBacked = true + foregroundNode.clipsToBounds = true + + var handleNodeImpl: ASImageNode? + var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton? + + switch scrubberHandle { + case .none: + break + case .line: + let handleNode = ASImageNode() + handleNode.image = generateHandleBackground(color: foregroundColor) + handleNode.isLayerBacked = true + handleNodeImpl = handleNode let handleNodeContainer = MediaPlayerScrubbingNodeButton() - handleNodeContainer.isUserInteractionEnabled = self.enableScrubbing + handleNodeContainer.addSubnode(handleNode) + handleNodeContainerImpl = handleNodeContainer + case .circle: + let handleNode = ASImageNode() + handleNode.image = generateFilledCircleImage(diameter: 7.0, color: foregroundColor) + handleNode.isLayerBacked = true + handleNodeImpl = handleNode - self.contentNodes = .custom(CustomMediaPlayerScrubbingNodeContentNode(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNodeContainer: handleNodeContainer)) + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + handleNodeContainer.addSubnode(handleNode) + handleNodeContainerImpl = handleNodeContainer + } + + handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing + + return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNode: handleNodeImpl, handleNodeContainer: handleNodeContainerImpl)) + case let .custom(backgroundNode, foregroundContentNode): + let foregroundNode = MediaPlayerScrubbingForegroundNode() + foregroundNode.isLayerBacked = true + foregroundNode.clipsToBounds = true + + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + handleNodeContainer.isUserInteractionEnabled = enableScrubbing + + return .custom(CustomMediaPlayerScrubbingNodeContentNode(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNodeContainer: handleNodeContainer)) } + } + + init(content: MediaPlayerScrubbingNodeContent) { + self.contentNodes = MediaPlayerScrubbingNode.contentNodesFromContent(content, enableScrubbing: self.enableScrubbing) super.init() - switch self.contentNodes { - case let .standard(node): - self.addSubnode(node.backgroundNode) - self.addSubnode(node.bufferingNode) - node.foregroundNode.addSubnode(node.foregroundContentNode) - self.addSubnode(node.foregroundNode) - - if let handleNodeContainer = node.handleNodeContainer { - self.addSubnode(handleNodeContainer) - handleNodeContainer.beginScrubbing = { [weak self] in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingBeginTimestamp = statusValue.timestamp - strongSelf.scrubbingTimestamp = statusValue.timestamp - strongSelf.updateProgress() - } - } - } - handleNodeContainer.updateScrubbing = { [weak self] addedFraction in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) - strongSelf.updateProgress() - } - } - } - handleNodeContainer.endScrubbing = { [weak self] apply in - if let strongSelf = self { - strongSelf.scrubbingBeginTimestamp = nil - let scrubbingTimestamp = strongSelf.scrubbingTimestamp - strongSelf.scrubbingTimestamp = nil - if let scrubbingTimestamp = scrubbingTimestamp, apply { - if let statusValue = strongSelf.statusValue { - strongSelf.ignoreSeekId = statusValue.seekId - } - strongSelf.seek?(scrubbingTimestamp) - } - strongSelf.updateProgress() - } - } - } - - node.foregroundNode.onEnterHierarchy = { [weak self] in - self?.updateProgress() - } - case let .custom(node): - self.addSubnode(node.backgroundNode) - node.foregroundNode.addSubnode(node.foregroundContentNode) - self.addSubnode(node.foregroundNode) - - if let handleNodeContainer = node.handleNodeContainer { - self.addSubnode(handleNodeContainer) - handleNodeContainer.beginScrubbing = { [weak self] in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingBeginTimestamp = statusValue.timestamp - strongSelf.scrubbingTimestamp = statusValue.timestamp - strongSelf.updateProgress() - } - } - } - handleNodeContainer.updateScrubbing = { [weak self] addedFraction in - if let strongSelf = self { - if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { - strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) - strongSelf.updateProgress() - } - } - } - handleNodeContainer.endScrubbing = { [weak self] apply in - if let strongSelf = self { - strongSelf.scrubbingBeginTimestamp = nil - let scrubbingTimestamp = strongSelf.scrubbingTimestamp - strongSelf.scrubbingTimestamp = nil - if let scrubbingTimestamp = scrubbingTimestamp, apply { - strongSelf.seek?(scrubbingTimestamp) - } - strongSelf.updateProgress() - } - } - } - - node.foregroundNode.onEnterHierarchy = { [weak self] in - self?.updateProgress() - } - } + self.setupContentNodes() self.statusDisposable = (self.statusValuePromise.get() - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.statusValue = status - } - }) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.statusValue = status + } + }) self.bufferingStatusDisposable = (self.bufferingStatusValuePromise.get() - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - switch strongSelf.contentNodes { - case let .standard(node): - if let status = status { - node.bufferingNode.updateStatus(status.0, status.1) - } - case .custom: - break + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + switch strongSelf.contentNodes { + case let .standard(node): + if let status = status { + node.bufferingNode.updateStatus(status.0, status.1) + } + case .custom: + break + } + } + }) + } + + private func setupContentNodes() { + if let subnodes = self.subnodes { + for subnode in subnodes { + subnode.removeFromSupernode() + } + } + + switch self.contentNodes { + case let .standard(node): + self.addSubnode(node.backgroundNode) + self.addSubnode(node.bufferingNode) + node.foregroundNode.addSubnode(node.foregroundContentNode) + self.addSubnode(node.foregroundNode) + + if let handleNodeContainer = node.handleNodeContainer { + self.addSubnode(handleNodeContainer) + handleNodeContainer.beginScrubbing = { [weak self] in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingBeginTimestamp = statusValue.timestamp + strongSelf.scrubbingTimestamp = statusValue.timestamp + strongSelf.updateProgress() + } } } - }) + handleNodeContainer.updateScrubbing = { [weak self] addedFraction in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) + strongSelf.updateProgress() + } + } + } + handleNodeContainer.endScrubbing = { [weak self] apply in + if let strongSelf = self { + strongSelf.scrubbingBeginTimestamp = nil + let scrubbingTimestamp = strongSelf.scrubbingTimestamp + strongSelf.scrubbingTimestamp = nil + if let scrubbingTimestamp = scrubbingTimestamp, apply { + if let statusValue = strongSelf.statusValue { + strongSelf.ignoreSeekId = statusValue.seekId + } + strongSelf.seek?(scrubbingTimestamp) + } + strongSelf.updateProgress() + } + } + } + + node.foregroundNode.onEnterHierarchy = { [weak self] in + self?.updateProgress() + } + case let .custom(node): + self.addSubnode(node.backgroundNode) + node.foregroundNode.addSubnode(node.foregroundContentNode) + self.addSubnode(node.foregroundNode) + + if let handleNodeContainer = node.handleNodeContainer { + self.addSubnode(handleNodeContainer) + handleNodeContainer.beginScrubbing = { [weak self] in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingBeginTimestamp = statusValue.timestamp + strongSelf.scrubbingTimestamp = statusValue.timestamp + strongSelf.updateProgress() + } + } + } + handleNodeContainer.updateScrubbing = { [weak self] addedFraction in + if let strongSelf = self { + if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) { + strongSelf.scrubbingTimestamp = scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction) + strongSelf.updateProgress() + } + } + } + handleNodeContainer.endScrubbing = { [weak self] apply in + if let strongSelf = self { + strongSelf.scrubbingBeginTimestamp = nil + let scrubbingTimestamp = strongSelf.scrubbingTimestamp + strongSelf.scrubbingTimestamp = nil + if let scrubbingTimestamp = scrubbingTimestamp, apply { + strongSelf.seek?(scrubbingTimestamp) + } + strongSelf.updateProgress() + } + } + } + + node.foregroundNode.onEnterHierarchy = { [weak self] in + self?.updateProgress() + } + } + } + + func updateContent(_ content: MediaPlayerScrubbingNodeContent) { + self.contentNodes = MediaPlayerScrubbingNode.contentNodesFromContent(content, enableScrubbing: self.enableScrubbing) + + self.setupContentNodes() + + self.updateProgress() } deinit { diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index 6e52b21aa6..335ade95ad 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -100,7 +100,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { switch strongSelf.mode { case .input: - interfaceInteraction.updateTextInputState { textInputState in + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var mentionQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.mention] { @@ -119,7 +119,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { let selectionPosition = range.lowerBound + (replacementText as NSString).length - return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } else if !peer.compactDisplayTitle.isEmpty { let replacementText = NSMutableAttributedString() replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)])) @@ -131,10 +131,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { let selectionPosition = updatedRange.lowerBound + replacementText.length - return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } } - return textInputState + return (textInputState, inputMode) } case .search: interfaceInteraction.beginMessageSearch(.member(peer), "") diff --git a/TelegramUI/MultiplexedSoftwareVideoSourceManager.swift b/TelegramUI/MultiplexedSoftwareVideoSourceManager.swift index 2c2f3f69c7..aa37f97aab 100644 --- a/TelegramUI/MultiplexedSoftwareVideoSourceManager.swift +++ b/TelegramUI/MultiplexedSoftwareVideoSourceManager.swift @@ -64,7 +64,7 @@ final class MultiplexedSoftwareVideoSourceManager { context.source = SoftwareVideoSource(path: data.path) } } - }), fetchStatusDisposable: self.account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) + }), fetchStatusDisposable: fetchedMediaResource(postbox: self.account.postbox, reference: AnyMediaReference.standalone(media: file).resourceReference(file.resource)).start()) } } } diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 99078dd91b..af6a04352b 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -26,11 +26,11 @@ private final class MultiplexedVideoTrackingNode: ASDisplayNode { } private final class VisibleVideoItem { - let file: TelegramMediaFile + let fileReference: FileMediaReference let frame: CGRect - init(file: TelegramMediaFile, frame: CGRect) { - self.file = file + init(fileReference: FileMediaReference, frame: CGRect) { + self.fileReference = fileReference self.frame = frame } } @@ -51,7 +51,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { } } - var files: [TelegramMediaFile] = [] { + var files: [FileMediaReference] = [] { didSet { self.updateVisibleItems() } @@ -66,7 +66,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { private let timebase: CMTimebase - var fileSelected: ((TelegramMediaFile) -> Void)? + var fileSelected: ((FileMediaReference) -> Void)? var enableVideoNodes = false init(account: Account) { @@ -186,17 +186,17 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { break; } - visibleThumbnailIds.insert(item.file.fileId) + visibleThumbnailIds.insert(item.fileReference.media.fileId) - if let thumbnailLayer = self.visibleThumbnailLayers[item.file.fileId] { + if let thumbnailLayer = self.visibleThumbnailLayers[item.fileReference.media.fileId] { if ensureFrames { thumbnailLayer.frame = item.frame } } else { - let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, file: item.file) + let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference) thumbnailLayer.frame = item.frame self.layer.addSublayer(thumbnailLayer) - self.visibleThumbnailLayers[item.file.fileId] = thumbnailLayer + self.visibleThumbnailLayers[item.fileReference.media.fileId] = thumbnailLayer } if item.frame.maxY < minVisibleY { @@ -206,9 +206,9 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { continue; } - visibleIds.insert(item.file.fileId) + visibleIds.insert(item.fileReference.media.fileId) - if let (_, layerHolder) = self.visibleLayers[item.file.fileId] { + if let (_, layerHolder) = self.visibleLayers[item.fileReference.media.fileId] { if ensureFrames { layerHolder.layer.frame = item.frame } @@ -217,11 +217,11 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.frame = item.frame self.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, resource: item.file.resource, layerHolder: layerHolder) - self.visibleLayers[item.file.fileId] = (manager, layerHolder) - self.visibleThumbnailLayers[item.file.fileId]?.ready = { [weak self] in + let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, resource: item.fileReference.media.resource, layerHolder: layerHolder) + self.visibleLayers[item.fileReference.media.fileId] = (manager, layerHolder) + self.visibleThumbnailLayers[item.fileReference.media.fileId]?.ready = { [weak self] in if let strongSelf = self { - strongSelf.visibleLayers[item.file.fileId]?.0.start() + strongSelf.visibleLayers[item.fileReference.media.fileId]?.0.start() } } } @@ -265,7 +265,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { var totalItemSize: CGFloat = 0.0 for item in self.files { let aspectRatio: CGFloat - if let dimensions = item.dimensions { + if let dimensions = item.media.dimensions { aspectRatio = dimensions.width / dimensions.height } else { aspectRatio = 1.0 @@ -302,7 +302,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { while j < n { let aspectRatio: CGFloat - if let dimensions = self.files[j].dimensions { + if let dimensions = self.files[j].media.dimensions { aspectRatio = dimensions.width / dimensions.height } else { aspectRatio = 1.0 @@ -328,7 +328,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { while j < n { let aspectRatio: CGFloat - if let dimensions = self.files[j].dimensions { + if let dimensions = self.files[j].media.dimensions { aspectRatio = dimensions.width / dimensions.height } else { aspectRatio = 1.0 @@ -342,7 +342,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { frame.size.width = max(1.0, maxWidth - frame.origin.x) } - displayItems.append(VisibleVideoItem(file: self.files[j], frame: frame)) + displayItems.append(VisibleVideoItem(fileReference: self.files[j], frame: frame)) offset.x += actualSize.width + minimumInteritemSpacing previousItemSize = actualSize.height @@ -378,22 +378,22 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { func frameForItem(_ id: MediaId) -> CGRect? { for item in self.displayItems { - if item.file.fileId == id { + if item.fileReference.media.fileId == id { return item.frame } } return nil } - func fileAt(point: CGPoint) -> TelegramMediaFile? { + func fileAt(point: CGPoint) -> FileMediaReference? { let offsetPoint = point.offsetBy(dx: 0.0, dy: self.bounds.minY) return self.offsetFileAt(point: offsetPoint) } - private func offsetFileAt(point: CGPoint) -> TelegramMediaFile? { + private func offsetFileAt(point: CGPoint) -> FileMediaReference? { for item in self.displayItems { if item.frame.contains(point) { - return item.file + return item.fileReference } } return nil diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index a7b437e80e..9df5762c18 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -39,7 +39,7 @@ enum NativeVideoContentId: Hashable { final class NativeVideoContent: UniversalVideoContent { let id: AnyHashable let nativeId: NativeVideoContentId - let file: TelegramMediaFile + let fileReference: FileMediaReference let dimensions: CGSize let duration: Int32 let streamVideo: Bool @@ -47,12 +47,12 @@ final class NativeVideoContent: UniversalVideoContent { let enableSound: Bool let fetchAutomatically: Bool - init(id: NativeVideoContentId, file: TelegramMediaFile, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, fetchAutomatically: Bool = true) { + init(id: NativeVideoContentId, fileReference: FileMediaReference, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, fetchAutomatically: Bool = true) { self.id = id self.nativeId = id - self.file = file - self.dimensions = file.dimensions ?? CGSize(width: 128.0, height: 128.0) - self.duration = file.duration ?? 0 + self.fileReference = fileReference + self.dimensions = fileReference.media.dimensions ?? CGSize(width: 128.0, height: 128.0) + self.duration = fileReference.media.duration ?? 0 self.streamVideo = streamVideo self.loopVideo = loopVideo self.enableSound = enableSound @@ -60,14 +60,14 @@ final class NativeVideoContent: UniversalVideoContent { } func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, file: self.file, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, fetchAutomatically: self.fetchAutomatically) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, fetchAutomatically: self.fetchAutomatically) } func isEqual(to other: UniversalVideoContent) -> Bool { if let other = other as? NativeVideoContent { if case let .message(_, stableId, _) = self.nativeId { if case .message(_, stableId, _) = other.nativeId { - if self.file.isInstantVideo { + if self.fileReference.media.isInstantVideo { return true } } @@ -79,7 +79,7 @@ final class NativeVideoContent: UniversalVideoContent { private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let postbox: Postbox - private let file: TelegramMediaFile + private let fileReference: FileMediaReference private let player: MediaPlayer private let imageNode: TransformImageNode private let playerNode: MediaPlayerNode @@ -108,13 +108,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var validLayout: CGSize? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, file: TelegramMediaFile, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, fetchAutomatically: Bool) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, fetchAutomatically: Bool) { self.postbox = postbox - self.file = file + self.fileReference = fileReference self.imageNode = TransformImageNode() - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: file.resource, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, fetchAutomatically: fetchAutomatically) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, fetchAutomatically: fetchAutomatically) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { self.player.actionAtEnd = .action({ @@ -128,7 +128,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.playerNode = MediaPlayerNode(backgroundThread: false) self.player.attachPlayerNode(self.playerNode) - self.dimensions = file.dimensions + self.dimensions = fileReference.media.dimensions super.init() @@ -136,7 +136,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self?.performActionAtEnd() } - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, video: file) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: fileReference) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { @@ -157,8 +157,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, seekId: status.seekId, status: status.status) }) - if let size = file.size { - self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(file.resource) |> map { ranges in + if let size = fileReference.media.size { + self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) |> map { ranges in return (ranges, size) }) } else { @@ -187,7 +187,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent if let dimensions = self.dimensions { let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) let makeLayout = self.imageNode.asyncLayout() - let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.file.isInstantVideo ? .clear : .white)) + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.fileReference.media.isInstantVideo ? .clear : .white)) applyLayout() } @@ -255,9 +255,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func fetchControl(_ control: UniversalVideoNodeFetchControl) { switch control { case .fetch: - self.fetchDisposable.set(self.postbox.mediaBox.fetchedResource(self.file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: MediaResourceStatsCategory.video)).start()) + self.fetchDisposable.set(fetchedMediaResource(postbox: self.postbox, reference: self.fileReference.resourceReference(self.fileReference.media.resource), statsCategory: statsCategoryForFileWithAttributes(self.fileReference.media.attributes)).start()) case .cancel: - self.postbox.mediaBox.cancelInteractiveResourceFetch(self.file.resource) + self.postbox.mediaBox.cancelInteractiveResourceFetch(self.fileReference.media.resource) } } } diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index 9afc37f5c9..b14abf8327 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -3,7 +3,12 @@ import Display import TelegramCore import Postbox -public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, animated: Bool = true) { +public enum NavigateToChatKeepStack { + case `default` + case always +} + +public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, animated: Bool = true) { var found = false var isFirst = true for controller in navigationController.viewControllers.reversed() { @@ -30,7 +35,14 @@ public func navigateToChatController(navigationController: NavigationController, } else { controller = ChatController(account: account, chatLocation: chatLocation, messageId: messageId, botStart: botStart) } - if account.telegramApplicationContext.immediateExperimentalUISettings.keepChatNavigationStack { + let resolvedKeepStack: Bool + switch keepStack { + case .default: + resolvedKeepStack = account.telegramApplicationContext.immediateExperimentalUISettings.keepChatNavigationStack + case .always: + resolvedKeepStack = true + } + if resolvedKeepStack { navigationController.pushViewController(controller) } else { navigationController.replaceAllButRootController(controller, animated: animated) @@ -38,6 +50,31 @@ public func navigateToChatController(navigationController: NavigationController, } } +private func findOpaqueLayer(rootLayer: CALayer, layer: CALayer) -> Bool { + if layer.isHidden || layer.opacity < 0.8 { + return false + } + + if !layer.isHidden, let backgroundColor = layer.backgroundColor, backgroundColor.alpha > 0.8 { + let coveringRect = layer.convert(layer.bounds, to: rootLayer) + let intersection = coveringRect.intersection(rootLayer.bounds) + let intersectionArea = intersection.width * intersection.height + let rootArea = rootLayer.bounds.width * rootLayer.bounds.height + if !rootArea.isZero && intersectionArea / rootArea > 0.8 { + return true + } + } + + if let sublayers = layer.sublayers { + for sublayer in sublayers { + if findOpaqueLayer(rootLayer: rootLayer, layer: sublayer) { + return true + } + } + } + return false +} + public func isOverlayControllerForChatNotificationOverlayPresentation(_ controller: ViewController) -> Bool { if controller is GalleryController || controller is AvatarGalleryController || controller is ThemeGalleryController || controller is InstantPageGalleryController { return true @@ -47,6 +84,10 @@ public func isOverlayControllerForChatNotificationOverlayPresentation(_ controll if let backgroundColor = controller.displayNode.backgroundColor, !backgroundColor.isEqual(UIColor.clear) { return true } + + if findOpaqueLayer(rootLayer: controller.view.layer, layer: controller.view.layer) { + return true + } } return false diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index 8ab1da530e..6b45299753 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -22,7 +22,7 @@ private enum ChatMessageGalleryControllerData { private func chatMessageGalleryControllerData(account: Account, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, synchronousLoad: Bool) -> ChatMessageGalleryControllerData? { var galleryMedia: Media? var otherMedia: Media? - var instantPageMedia: [InstantPageGalleryEntry]? + var instantPageMedia: (TelegramMediaWebpage, [InstantPageGalleryEntry])? for media in message.media { if let file = media as? TelegramMediaFile { galleryMedia = file @@ -42,7 +42,7 @@ private func chatMessageGalleryControllerData(account: Account, message: Message case .instagram, .twitter: let medias = instantPageGalleryMedia(webpageId: webpage.webpageId, page: instantPage, galleryMedia: galleryMedia) if medias.count > 1 { - instantPageMedia = medias + instantPageMedia = (webpage, medias) } case .generic: break @@ -56,7 +56,7 @@ private func chatMessageGalleryControllerData(account: Account, message: Message } } - if let instantPageMedia = instantPageMedia, let galleryMedia = galleryMedia { + if let (webPage, instantPageMedia) = instantPageMedia, let galleryMedia = galleryMedia { var centralIndex: Int = 0 for i in 0 ..< instantPageMedia.count { if instantPageMedia[i].media.media.id == galleryMedia.id { @@ -65,7 +65,7 @@ private func chatMessageGalleryControllerData(account: Account, message: Message } } - let gallery = InstantPageGalleryController(account: account, entries: instantPageMedia, centralIndex: centralIndex, replaceRootController: { [weak navigationController] controller, ready in + let gallery = InstantPageGalleryController(account: account, webPage: webPage, entries: instantPageMedia, centralIndex: centralIndex, replaceRootController: { [weak navigationController] controller, ready in if let navigationController = navigationController { navigationController.replaceTopController(controller, animated: false, ready: ready) } @@ -141,7 +141,7 @@ func chatMessagePreviewControllerData(account: Account, message: Message, standa return nil } -func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> UIView?)?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((TelegramMediaFile) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { +func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> UIView?)?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, sendSticker: ((FileMediaReference) -> Void)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { if let mediaData = chatMessageGalleryControllerData(account: account, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, synchronousLoad: false) { switch mediaData { case let .url(url): diff --git a/TelegramUI/OverlayMediaControllerNode.swift b/TelegramUI/OverlayMediaControllerNode.swift index 5386502918..2f219019c3 100644 --- a/TelegramUI/OverlayMediaControllerNode.swift +++ b/TelegramUI/OverlayMediaControllerNode.swift @@ -95,6 +95,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega private func nodePosition(layout: ContainerViewLayout, size: CGSize, location: CGPoint, hidden: Bool, isMinimized: Bool, tempExtendedTopInset: Bool) -> CGPoint { var layoutInsets = layout.insets(options: [.input]) + layoutInsets.top += 37.0 layoutInsets.bottom += 48.0 if tempExtendedTopInset { layoutInsets.top += 38.0 diff --git a/TelegramUI/OverlayPlayerControlsNode.swift b/TelegramUI/OverlayPlayerControlsNode.swift index dba16e98d8..bb392d6d3f 100644 --- a/TelegramUI/OverlayPlayerControlsNode.swift +++ b/TelegramUI/OverlayPlayerControlsNode.swift @@ -263,9 +263,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if let source = value?.item.playbackData?.source { switch source { - case let .telegramFile(file): - if let size = file.size { - strongSelf.scrubberNode.bufferingStatus = postbox.mediaBox.resourceRangesStatus(file.resource) + case let .telegramFile(fileReference): + if let size = fileReference.media.size { + strongSelf.scrubberNode.bufferingStatus = postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) |> map { ranges -> (IndexSet, Int) in return (ranges, size) } diff --git a/TelegramUI/OverlayStatusController.swift b/TelegramUI/OverlayStatusController.swift index e62d3e223b..072a2cfe53 100644 --- a/TelegramUI/OverlayStatusController.swift +++ b/TelegramUI/OverlayStatusController.swift @@ -5,6 +5,7 @@ import LegacyComponents enum OverlayStatusControllerType { case success + case proxySettingSuccess } private final class OverlayStatusControllerNode: ViewControllerTracingNode { diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 1db5276414..45585de8e7 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -19,52 +19,57 @@ private let roundCorners = { () -> UIImage in return image }() -func peerAvatarImage(account: Account, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { +func peerAvatarImage(account: Account, peer: Peer, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) let imageData = resourceData - |> take(1) - |> mapToSignal { maybeData -> Signal in - if maybeData.complete { - return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) - } else { - return Signal { subscriber in - let resourceDataDisposable = resourceData.start(next: { data in - if data.complete { - subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) - subscriber.putCompletion() - } else { - subscriber.putNext(nil) - } - }, error: { error in - subscriber.putError(error) - }, completed: { + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + } else { + return Signal { subscriber in + let resourceDataDisposable = resourceData.start(next: { data in + if data.complete { + subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) subscriber.putCompletion() - }) - let fetchedDataDisposable = account.postbox.mediaBox.fetchedResource(smallProfileImage.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start() - return ActionDisposable { - resourceDataDisposable.dispose() - fetchedDataDisposable.dispose() + } else { + subscriber.putNext(nil) } + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + var fetchedDataDisposable: Disposable? + if let peerReference = PeerReference(peer) { + fetchedDataDisposable = fetchedMediaResource(postbox: account.postbox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start() + } else { + fetchedDataDisposable = fetchedMediaResource(postbox: account.postbox, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start() + } + return ActionDisposable { + resourceDataDisposable.dispose() + fetchedDataDisposable?.dispose() } } } + } return imageData - |> deliverOn(account.graphicsThreadPool) - |> map { data -> UIImage? in - if let data = data, let image = generateImage(displayDimensions, contextGenerator: { size, context -> Void in - if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { - context.setBlendMode(.copy) - context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions)) - context.setBlendMode(.destinationOut) - context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions)) - } - }) { - return image - } else { - return nil + |> deliverOn(account.graphicsThreadPool) + |> map { data -> UIImage? in + if let data = data, let image = generateImage(displayDimensions, contextGenerator: { size, context -> Void in + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + context.setBlendMode(.copy) + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions)) + context.setBlendMode(.destinationOut) + context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions)) } + }) { + return image + } else { + return nil } + } } else { return nil } diff --git a/TelegramUI/PeerAvatarImageGalleryItem.swift b/TelegramUI/PeerAvatarImageGalleryItem.swift index 0c643249e8..6d746a5190 100644 --- a/TelegramUI/PeerAvatarImageGalleryItem.swift +++ b/TelegramUI/PeerAvatarImageGalleryItem.swift @@ -5,14 +5,33 @@ import SwiftSignalKit import Postbox import TelegramCore +private enum PeerAvatarImageGalleryThumbnailContent: Equatable { + case avatar(PeerReference, [TelegramMediaImageRepresentation]) + case standaloneImage([TelegramMediaImageRepresentation]) + + var representations: [TelegramMediaImageRepresentation] { + switch self { + case let .avatar(_, representations): + return representations + case let .standaloneImage(representations): + return representations + } + } +} + private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { let account: Account - let representations: [TelegramMediaImageRepresentation] + let peer: Peer + let content: PeerAvatarImageGalleryThumbnailContent var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: self.representations, reference: nil) - if let representation = largestImageRepresentation(image.representations) { - return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions) + if let representation = largestImageRepresentation(self.content.representations) { + switch self.content { + case let .avatar(peer, representations): + return (avatarGalleryThumbnailPhoto(account: self.account, representations: representations.map({ ($0, .avatar(peer: peer, resource: $0.resource)) })), representation.dimensions) + case let .standaloneImage(representations): + return (avatarGalleryThumbnailPhoto(account: self.account, representations: representations.map({ ($0, .standalone(resource: $0.resource)) })), representation.dimensions) + } } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } @@ -20,7 +39,7 @@ private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { func isEqual(to: GalleryThumbnailItem) -> Bool { if let to = to as? PeerAvatarImageGalleryThumbnailItem { - return self.representations == to.representations + return self.content == to.content } else { return false } @@ -29,19 +48,21 @@ private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { class PeerAvatarImageGalleryItem: GalleryItem { let account: Account + let peer: Peer let strings: PresentationStrings let entry: AvatarGalleryEntry let delete: (() -> Void)? - init(account: Account, strings: PresentationStrings, entry: AvatarGalleryEntry, delete: (() -> Void)?) { + init(account: Account, peer: Peer, strings: PresentationStrings, entry: AvatarGalleryEntry, delete: (() -> Void)?) { self.account = account + self.peer = peer self.strings = strings self.entry = entry self.delete = delete } func node() -> GalleryItemNode { - let node = PeerAvatarImageGalleryItemNode(account: self.account) + let node = PeerAvatarImageGalleryItemNode(account: self.account, peer: self.peer) if let indexData = self.entry.indexData { node._title.set(.single("\(indexData.position + 1) \(self.strings.Common_of) \(indexData.totalCount)")) @@ -65,12 +86,25 @@ class PeerAvatarImageGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return (0, PeerAvatarImageGalleryThumbnailItem(account: self.account, representations: self.entry.representations)) + let content: PeerAvatarImageGalleryThumbnailContent + switch self.entry { + case let .topImage(representations, _): + if let peerReference = PeerReference(self.peer) { + content = .avatar(peerReference, representations) + } else { + return nil + } + case let .image(image, _): + content = .standaloneImage(image.representations) + } + + return (0, PeerAvatarImageGalleryThumbnailItem(account: self.account, peer: self.peer, content: content)) } } final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let account: Account + private let peer: Peer private var entry: AvatarGalleryEntry? @@ -85,8 +119,9 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private var status: MediaResourceStatus? - init(account: Account) { + init(account: Account, peer: Peer) { self.account = account + self.peer = peer self.imageNode = TransformImageNode() self.footerContentNode = AvatarGalleryItemFooterContentNode(account: account) @@ -142,9 +177,26 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let largestSize = largestImageRepresentation(entry.representations) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: entry.representations), dispatchOnDisplayLink: false) + let representations: [(TelegramMediaImageRepresentation, MediaResourceReference)] + switch entry { + case let .topImage(topRepresentations, _): + if let peerReference = PeerReference(self.peer) { + representations = topRepresentations.map { representation in + return (representation, .avatar(peer: peerReference, resource: representation.resource)) + } + } else { + representations = [] + } + case let .image(image, _): + representations = image.representations.map { representation in + return (representation, .standalone(resource: representation.resource)) + } + } + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: representations), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + if let largestIndex = representations.index(where: { $0.0 == largestSize }) { + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: representations[largestIndex].1).start()) + } self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(largestSize.resource) |> deliverOnMainQueue).start(next: { [weak self] status in @@ -299,12 +351,30 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } @objc func statusPressed() { - if let entry = self.entry, let resource = largestImageRepresentation(entry.representations)?.resource, let status = self.status { + if let entry = self.entry, let largestSize = largestImageRepresentation(entry.representations), let status = self.status { switch status { case .Fetching: - self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource) + self.account.postbox.mediaBox.cancelInteractiveResourceFetch(largestSize.resource) case .Remote: - self.fetchDisposable.set(self.account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + let representations: [(TelegramMediaImageRepresentation, MediaResourceReference)] + switch entry { + case let .topImage(topRepresentations, _): + if let peerReference = PeerReference(self.peer) { + representations = topRepresentations.map { representation in + return (representation, .avatar(peer: peerReference, resource: representation.resource)) + } + } else { + representations = [] + } + case let .image(image, _): + representations = image.representations.map { representation in + return (representation, .standalone(resource: representation.resource)) + } + } + + if let largestIndex = representations.index(where: { $0.0 == largestSize }) { + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: representations[largestIndex].1).start()) + } default: break } diff --git a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift index 1d8f21bf2b..3fac79de49 100644 --- a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift +++ b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift @@ -13,9 +13,9 @@ enum PeerChannelMemberContextKey: Hashable { private final class PeerChannelMemberCategoriesContextsManagerImpl { fileprivate var contexts: [PeerId: PeerChannelMemberCategoriesContext] = [:] - func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { if let current = self.contexts[peerId] { - return current.getContext(key: key, updated: updated) + return current.getContext(key: key, requestUpdate: requestUpdate, updated: updated) } else { var becameEmptyImpl: ((Bool) -> Void)? let context = PeerChannelMemberCategoriesContext(postbox: postbox, network: network, peerId: peerId, becameEmpty: { value in @@ -30,7 +30,7 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { } } self.contexts[peerId] = context - return context.getContext(key: key, updated: updated) + return context.getContext(key: key, requestUpdate: requestUpdate, updated: updated) } } @@ -58,10 +58,10 @@ final class PeerChannelMemberCategoriesContextsManager { } } - private func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + private func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { assert(Queue.mainQueue().isCurrent()) if let (disposable, control) = self.impl.syncWith({ impl in - return impl.getContext(postbox: postbox, network: network, peerId: peerId, key: key, updated: updated) + return impl.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) }) { return (disposable, control) } else { @@ -69,22 +69,22 @@ final class PeerChannelMemberCategoriesContextsManager { } } - func recent(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + func recent(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { let key: PeerChannelMemberContextKey if let searchQuery = searchQuery { key = .recentSearch(searchQuery) } else { key = .recent } - return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, updated: updated) + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) } func admins(postbox: Postbox, network: Network, peerId: PeerId, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins, updated: updated) + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins, requestUpdate: true, updated: updated) } func restrictedAndBanned(postbox: Postbox, network: Network, peerId: PeerId, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .restrictedAndBanned, updated: updated) + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .restrictedAndBanned, requestUpdate: true, updated: updated) } func updateMemberBannedRights(account: Account, peerId: PeerId, memberId: PeerId, bannedRights: TelegramChannelBannedRights?) -> Signal { diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift deleted file mode 100644 index d245b22c1d..0000000000 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ /dev/null @@ -1,222 +0,0 @@ -import Foundation -import Postbox -import TelegramCore -import SwiftSignalKit - -struct PeerMessageHistoryAudioPlaylistItemId: AudioPlaylistItemId { - let id: MessageId - - var hashValue: Int { - return self.id.hashValue - } - - func isEqual(to: AudioPlaylistItemId) -> Bool { - if let other = to as? PeerMessageHistoryAudioPlaylistItemId { - return self.id == other.id - } else { - return false - } - } -} - -final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { - let entry: MessageHistoryEntry - - var id: AudioPlaylistItemId { - return PeerMessageHistoryAudioPlaylistItemId(id: self.entry.index.id) - } - - var resource: MediaResource? { - switch self.entry { - case let .MessageEntry(message, _, _, _): - for media in message.media { - if let file = media as? TelegramMediaFile { - return file.resource - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { - return file.resource - } - } - return nil - case .HoleEntry: - return nil - } - } - - var streamable: Bool { - switch self.entry { - case let .MessageEntry(message, _, _, _): - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isMusic { - return true - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { - if file.isMusic { - return true - } - } - } - } - return false - case .HoleEntry: - return false - } - } - - var info: AudioPlaylistItemInfo? { - switch self.entry { - case let .MessageEntry(message, _, _, _): - for media in message.media { - if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, duration, title, performer, _): - if isVoice { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) - } else { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) - } - case let .Video(duration, _, flags): - if flags.contains(.instantRoundVideo) { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) - } - default: - break - } - } - return nil - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { - for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, duration, title, performer, _): - if isVoice { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) - } else { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) - } - case let .Video(duration, _, flags): - if flags.contains(.instantRoundVideo) { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) - } - default: - break - } - } - return nil - } - } - case .HoleEntry: - break - } - - return nil - } - - init(entry: MessageHistoryEntry) { - self.entry = entry - } - - func isEqual(to: AudioPlaylistItem) -> Bool { - if let other = to as? PeerMessageHistoryAudioPlaylistItem { - return self.entry == other.entry - } else { - return false - } - } -} - -struct PeerMessageHistoryAudioPlaylistId: AudioPlaylistId { - let peerId: PeerId - - func isEqual(to: AudioPlaylistId) -> Bool { - if let other = to as? PeerMessageHistoryAudioPlaylistId { - if self.peerId != other.peerId { - return false - } - return true - } else { - return false - } - } -} - -func peerMessageAudioPlaylistAndItemIds(_ message: Message) -> (AudioPlaylistId, AudioPlaylistItemId)? { - return (PeerMessageHistoryAudioPlaylistId(peerId: message.id.peerId), PeerMessageHistoryAudioPlaylistItemId(id: message.id)) -} - -func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> AudioPlaylist { - return AudioPlaylist(id: PeerMessageHistoryAudioPlaylistId(peerId: messageId.peerId), navigate: { item, navigation in - if let item = item as? PeerMessageHistoryAudioPlaylistItem { - var tagMask: MessageTags? - switch item.entry { - case let .MessageEntry(message, _, _, _): - for media in message.media { - if let file = media as? TelegramMediaFile { - inner: for attribute in file.attributes { - switch attribute { - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - tagMask = .voiceOrInstantVideo - break inner - } - case let .Audio(isVoice, _, _, _, _): - if isVoice { - tagMask = .voiceOrInstantVideo - } else { - tagMask = .music - } - break inner - default: - break - } - } - break - } - } - case .HoleEntry: - break - } - if let tagMask = tagMask { - return account.postbox.aroundMessageHistoryViewForLocation(.peer(item.entry.index.id.peerId), index: .message(item.entry.index), anchorIndex: .message(item.entry.index), count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) - |> take(1) - |> map { (view, _, _) -> AudioPlaylistItem? in - var index = 0 - for entry in view.entries { - if entry.index.id == item.entry.index.id { - switch navigation { - case .previous: - if index != 0 { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index - 1]) - } else { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) - } - case .next: - if index + 1 < view.entries.count { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index + 1]) - } else { - return nil//PeerMessageHistoryAudioPlaylistItem(entry: view.entries.last!) - } - } - } - index += 1 - } - if !view.entries.isEmpty { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) - } else { - return nil - } - } - } else { - return .single(nil) - } - } else { - return account.postbox.messageAtId(messageId) - |> map { message -> AudioPlaylistItem? in - if let message = message { - return PeerMessageHistoryAudioPlaylistItem(entry: .MessageEntry(message, false, nil, nil)) - } else { - return nil - } - } - } - }) -} diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 39719a8fe3..02a34ad840 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -36,6 +36,8 @@ public class PeerMediaCollectionController: TelegramController { private var presentationData: PresentationData + private var resolveUrlDisposable: MetaDisposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId @@ -80,11 +82,7 @@ public class PeerMediaCollectionController: TelegramController { strongSelf.mediaCollectionDisplayNode.view.insertSubview(view, aboveSubview: strongSelf.mediaCollectionDisplayNode.historyNode.view) } }, openUrl: { url in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.applicationBindings.openUrl(url) - } - } + self?.openUrl(url) }, openPeer: { peer, navigation in self?.controllerInteraction?.openPeer(peer.id, navigation, nil) }, callPeer: { peerId in @@ -157,11 +155,7 @@ public class PeerMediaCollectionController: TelegramController { }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url in - if let strongSelf = self { - if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.applicationBindings.openUrl(url) - } - } + self?.openUrl(url) }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in @@ -284,6 +278,7 @@ public class PeerMediaCollectionController: TelegramController { self?.present(c, in: .window(.root), with: a) }), in: .window(.root)) } + }, reportMessages: { _ in }, deleteMessages: { _ in }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { @@ -345,7 +340,7 @@ public class PeerMediaCollectionController: TelegramController { } }) } - }, updateTextInputState: { _ in + }, updateTextInputStateAndMode: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { }, beginMessageSearch: { _, _ in @@ -411,6 +406,7 @@ public class PeerMediaCollectionController: TelegramController { self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() self.messageContextDisposable.dispose() + self.resolveUrlDisposable?.dispose() } var mediaCollectionDisplayNode: PeerMediaCollectionControllerNode { @@ -547,4 +543,44 @@ public class PeerMediaCollectionController: TelegramController { self.mediaCollectionDisplayNode.deactivateSearch() } } + + private func openUrl(_ url: String) { + let disposable: MetaDisposable + if let current = self.resolveUrlDisposable { + disposable = current + } else { + disposable = MetaDisposable() + self.resolveUrlDisposable = disposable + } + disposable.set((resolveUrl(account: self.account, url: url) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.navigationController as? NavigationController, openPeer: { peerId, navigation in + if let strongSelf = self { + switch navigation { + case let .chat(_, messageId): + if let navigationController = strongSelf.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always) + } + case .info: + strongSelf.navigationActionDisposable.set((strongSelf.account.postbox.loadedPeerWithId(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, peer.restrictionText == nil { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + })) + case let .withBotStartPayload(startPayload): + if let navigationController = strongSelf.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), botStart: startPayload) + } + } + } + }, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }) + } + })) + } } diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift index 09376e42a5..213422db1a 100644 --- a/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -40,6 +40,20 @@ struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId { } } +private func extractFileMedia(_ message: Message) -> TelegramMediaFile? { + var file: TelegramMediaFile? + for media in message.media { + if let media = media as? TelegramMediaFile { + file = media + break + } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content, let f = content.file { + file = f + break + } + } + return file +} + final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { let id: SharedMediaPlaylistItemId let message: Message @@ -54,34 +68,34 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } var playbackData: SharedMediaPlaybackData? { - for media in self.message.media { - if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, _, _, _, _): - if isVoice { - return SharedMediaPlaybackData(type: .voice, source: .telegramFile(file)) - } else { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(file)) - } - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(file)) - } else { - return nil - } - default: - break - } + if let file = extractFileMedia(self.message) { + let fileReference = FileMediaReference.message(message: MessageReference(self.message), media: file) + let source = SharedMediaPlaybackDataSource.telegramFile(fileReference) + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, _, _, _, _): + if isVoice { + return SharedMediaPlaybackData(type: .voice, source: source) + } else { + return SharedMediaPlaybackData(type: .music, source: source) + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + return SharedMediaPlaybackData(type: .instantVideo, source: source) + } else { + return nil + } + default: + break } - if file.mimeType.hasPrefix("audio/") { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(file)) - } - if let fileName = file.fileName { - let ext = (fileName as NSString).pathExtension.lowercased() - if ext == "wav" || ext == "opus" { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(file)) - } + } + if file.mimeType.hasPrefix("audio/") { + return SharedMediaPlaybackData(type: .music, source: source) + } + if let fileName = file.fileName { + let ext = (fileName as NSString).pathExtension.lowercased() + if ext == "wav" || ext == "opus" { + return SharedMediaPlaybackData(type: .music, source: source) } } } @@ -89,34 +103,32 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { } var displayData: SharedMediaPlaybackDisplayData? { - for media in self.message.media { - if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - switch attribute { - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - return SharedMediaPlaybackDisplayData.voice(author: self.message.author, peer: self.message.peers[self.message.id.peerId]) - } else { - var updatedTitle = title - let updatedPerformer = performer - if (title ?? "").isEmpty && (performer ?? "").isEmpty { - updatedTitle = file.fileName ?? "" - } - return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))) + if let file = extractFileMedia(self.message) { + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return SharedMediaPlaybackDisplayData.voice(author: self.message.author, peer: self.message.peers[self.message.id.peerId]) + } else { + var updatedTitle = title + let updatedPerformer = performer + if (title ?? "").isEmpty && (performer ?? "").isEmpty { + updatedTitle = file.fileName ?? "" } - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.author, peer: self.message.peers[self.message.id.peerId]) - } else { - return nil - } - default: - break - } + return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))) + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.author, peer: self.message.peers[self.message.id.peerId], timestamp: self.message.timestamp) + } else { + return nil + } + default: + break } - - return SharedMediaPlaybackDisplayData.music(title: file.fileName ?? "", performer: self.message.author?.displayTitle ?? "", albumArt: nil) } + + return SharedMediaPlaybackDisplayData.music(title: file.fileName ?? "", performer: self.message.author?.displayTitle ?? "", albumArt: nil) } return nil } @@ -239,13 +251,11 @@ struct PeerMessagesMediaPlaylistId: SharedMediaPlaylistId { } func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? { - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVoice || file.isInstantVideo { - return .voice - } else if file.isMusic { - return .music - } + if let file = extractFileMedia(message) { + if file.isVoice || file.isInstantVideo { + return .voice + } else if file.isMusic { + return .music } } return nil @@ -324,7 +334,14 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { case .random: navigation = .random } - self.loadItem(anchor: .index(MessageIndex(currentItem)), navigation: navigation) + + if case .singleMessage = self.messagesLocation { + self.loadingItem = false + self.currentItem = nil + self.updateState() + } else { + self.loadItem(anchor: .index(MessageIndex(currentItem)), navigation: navigation) + } } } } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 741e334754..3e43cf7c28 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -16,8 +16,8 @@ func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMedia return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0)) } -private func chatMessagePhotoDatas(postbox: Postbox, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { +private func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in @@ -25,8 +25,8 @@ private func chatMessagePhotoDatas(postbox: Postbox, photo: TelegramMediaImage, let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - let fetchedFullSize = postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(postbox: postbox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -82,19 +82,21 @@ private func chatMessagePhotoDatas(postbox: Postbox, photo: TelegramMediaImage, } } -private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { - let thumbnailResource = smallestImageRepresentation(file.previewRepresentations)?.resource - let fullSizeResource = file.resource +private func chatMessageFileDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { + let thumbnailResource = smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource + let fullSizeResource = fileReference.media.resource let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in if maybeData.complete { return .single((nil, maybeData.path, true)) } else { let fetchedThumbnail: Signal if let thumbnailResource = thumbnailResource { - fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes)) } else { fetchedThumbnail = .complete() } @@ -139,12 +141,12 @@ private let thumbnailGenerationMimeTypes: Set = Set([ "image/heic" ]) -private func chatMessageImageFileThumbnailDatas(account: Account, file: TelegramMediaFile, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { - let thumbnailResource = smallestImageRepresentation(file.previewRepresentations)?.resource +private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { + let thumbnailResource = smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource - if !thumbnailGenerationMimeTypes.contains(file.mimeType) { + if !thumbnailGenerationMimeTypes.contains(fileReference.media.mimeType) { if let thumbnailResource = thumbnailResource { - let fetchedThumbnail: Signal = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + let fetchedThumbnail: Signal = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) return Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in @@ -161,7 +163,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, file: Telegram } } - let fullSizeResource: MediaResource = file.resource + let fullSizeResource: MediaResource = fileReference.media.resource let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) @@ -171,7 +173,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, file: Telegram } else { let fetchedThumbnail: Signal if let thumbnailResource = thumbnailResource { - fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) } else { fetchedThumbnail = .complete() } @@ -208,10 +210,10 @@ private func chatMessageImageFileThumbnailDatas(account: Account, file: Telegram return signal } -private func chatMessageVideoDatas(postbox: Postbox, file: TelegramMediaFile, thumbnailSize: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { +private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaReference, thumbnailSize: Bool = false) -> Signal<(Data?, (Data, String)?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource - let fullSizeResource = file.resource + let fullSizeResource = fileReference.media.resource let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false) @@ -221,7 +223,7 @@ private func chatMessageVideoDatas(postbox: Postbox, file: TelegramMediaFile, th return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), true)) } else { - let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)) + let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -366,7 +368,7 @@ private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:]) private var cachedTails = Atomic<[Tail: DrawingContext]>(value: [:]) private func cornerContext(_ corner: Corner) -> DrawingContext { - var cached: DrawingContext? = cachedCorners.with { + let cached: DrawingContext? = cachedCorners.with { return $0[corner] } @@ -403,7 +405,7 @@ private func cornerContext(_ corner: Corner) -> DrawingContext { } private func tailContext(_ tail: Tail) -> DrawingContext { - var cached: DrawingContext? = cachedTails.with { + let cached: DrawingContext? = cachedTails.with { return $0[tail] } @@ -445,18 +447,6 @@ private func tailContext(_ tail: Tail) -> DrawingContext { c.addLine(to: CGPoint(x: 9.0, y: 12.5)) c.closePath() c.fillPath() - - /*CGContextMoveToPoint(c, 3.0, 0.0) - CGContextAddLineToPoint(c, 3.0, 8.7) - CGContextAddLineToPoint(c, 2.0, 11.7) - CGContextAddLineToPoint(c, 1.5, 12.7) - CGContextAddLineToPoint(c, 0.8, 13.7) - CGContextAddLineToPoint(c, 0.2, 14.4) - CGContextAddLineToPoint(c, 3.5, 13.8) - CGContextAddLineToPoint(c, 5.0, 13.2) - CGContextAddLineToPoint(c, 3.0 + CGFloat(radius) - 9.5, 11.5) - CGContextClosePath(c) - CGContextFillPath(c)*/ } c.fillEllipse(in: rect) } @@ -533,8 +523,8 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu } } -func rawMessagePhoto(postbox: Postbox, photo: TelegramMediaImage) -> Signal { - return chatMessagePhotoDatas(postbox: postbox, photo: photo, autoFetchFullSize: true) +func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal { + return chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, autoFetchFullSize: true) |> map { (thumbnailData, fullSizeData, fullSizeComplete) -> UIImage? in if let fullSizeData = fullSizeData { if fullSizeComplete { @@ -548,8 +538,8 @@ func rawMessagePhoto(postbox: Postbox, photo: TelegramMediaImage) -> Signal Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(postbox: postbox, photo: photo) +func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -663,18 +653,20 @@ func chatMessagePhoto(postbox: Postbox, photo: TelegramMediaImage) -> Signal<(Tr } } -private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMediaImage) -> Signal<(Data?, Data?, Bool), NoError> { +private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: ImageMediaReference) -> Signal<(Data?, Data?, Bool), NoError> { let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) - if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { + if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -708,8 +700,8 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMed } } -func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoThumbnailDatas(account: account, photo: photo) +func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoThumbnailDatas(account: account, photoReference: photoReference) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -795,10 +787,11 @@ func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage) -> S } } -func chatMessageVideoThumbnail(account: Account, file: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(postbox: account.postbox, file: file, thumbnailSize: true) +func chatMessageVideoThumbnail(account: Account, fileReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageVideoDatas(postbox: account.postbox, fileReference: fileReference, thumbnailSize: true) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return signal + |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -882,8 +875,8 @@ func chatMessageVideoThumbnail(account: Account, file: TelegramMediaFile) -> Sig } } -func chatSecretPhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(postbox: account.postbox, photo: photo) +func chatSecretPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -984,8 +977,75 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tra } } -func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(postbox: account.postbox, photo: photo, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) +private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [(TelegramMediaImageRepresentation, MediaResourceReference)], fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.0 })), let largestRepresentation = imageRepresentationLargerThan(representations.map({ $0.0 }), size: fullRepresentationSize), let smallestIndex = representations.index(where: { $0.0 == smallestRepresentation }), let largestIndex = representations.index(where: { $0.0 == largestRepresentation }) { + + let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource) + + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single((nil, loadedData, true)) + } else { + let fetchedThumbnail = fetchedMediaResource(postbox: postbox, reference: representations[smallestIndex].1, statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(postbox: postbox, reference: representations[largestIndex].1, statsCategory: .image) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + let fullSizeData: Signal<(Data?, Bool), NoError> + + if autoFetchFullSize { + fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + } else { + fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource) + |> map { next -> (Data?, Bool) in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } + } + + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeData |> map { (fullSizeData, complete) in + return (thumbnailData, fullSizeData, complete) + } + } + } + } |> distinctUntilChanged(isEqual: { lhs, rhs in + if (lhs.0 == nil && lhs.1 == nil) && (rhs.0 == nil && rhs.1 == nil) { + return true + } else { + return false + } + }) + + return signal + } else { + return .never() + } +} + +func avatarGalleryThumbnailPhoto(account: Account, representations: [(TelegramMediaImageRepresentation, MediaResourceReference)]) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = avatarGalleryThumbnailDatas(postbox: account.postbox, representations: representations, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -1065,8 +1125,89 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa } } -func gifPaneVideoThumbnail(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - if let smallestRepresentation = smallestImageRepresentation(video.previewRepresentations) { +func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) + + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return { arguments in + assertNotOnMainThread() + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + var imageOrientation: UIImageOrientation = .up + if let fullSizeData = fullSizeData { + if fullSizeComplete { + let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image + } + } else { + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image + } + } + } + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + thumbnailImage = image + } + + var blurredThumbnailImage: UIImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.boundingSize != arguments.imageSize { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + c.interpolationQuality = .low + drawImage(context: c, image: cgImage, orientation: imageOrientation, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} + +func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + if let smallestRepresentation = smallestImageRepresentation(videoReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource let thumbnail = Signal { subscriber in @@ -1075,7 +1216,7 @@ func gifPaneVideoThumbnail(account: Account, video: TelegramMediaFile) -> Signal }, completed: { subscriber.putCompletion() }) - let fetched = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start() + let fetched = fetchedMediaResource(postbox: account.postbox, reference: videoReference.resourceReference(thumbnailResource)).start() return ActionDisposable { data.dispose() fetched.dispose() @@ -1083,67 +1224,69 @@ func gifPaneVideoThumbnail(account: Account, video: TelegramMediaFile) -> Signal } return thumbnail - |> map { data in - let thumbnailData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) - let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { - thumbnailImage = image - } - - var blurredThumbnailImage: UIImage? - if let thumbnailImage = thumbnailImage { - let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withFlippedContext { c in - c.interpolationQuality = .none - c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - - blurredThumbnailImage = thumbnailContext.generateImage() - } - - context.withFlippedContext { c in - c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { - c.fill(arguments.drawingRect) - } - - c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { - c.interpolationQuality = .low - drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) - c.setBlendMode(.normal) - } - } - - addCorners(context, arguments: arguments) - - return context + |> map { data in + let thumbnailData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + thumbnailImage = image } + + var blurredThumbnailImage: UIImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.boundingSize != arguments.imageSize { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + c.interpolationQuality = .low + drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) + c.setBlendMode(.normal) + } + } + + addCorners(context, arguments: arguments) + + return context } + } } else { return .never() } } -func mediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return internalMediaGridMessageVideo(postbox: postbox, video: video) |> map { +func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference) + |> map { return $0.1 } } -func internalMediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { - let signal = chatMessageVideoDatas(postbox: postbox, file: video) +func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { + let signal = chatMessageVideoDatas(postbox: postbox, fileReference: videoReference) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return signal + |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return ({ var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { @@ -1269,39 +1412,60 @@ func internalMediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) - } } -func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo) { +func chatMessagePhotoStatus(account: Account, photoReference: ImageMediaReference) -> Signal { + if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource) } else { return .never() } } -func chatMessagePhotoInteractiveFetched(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo) { - return account.postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) +func chatMessagePhotoInteractiveFetched(account: Account, photoReference: ImageMediaReference) -> Signal { + if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { + return fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) } else { return .never() } } -func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) { - if let largestRepresentation = largestRepresentationForPhoto(photo) { +func chatMessagePhotoCancelInteractiveFetch(account: Account, photoReference: ImageMediaReference) { + if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) } } func chatMessageWebFileInteractiveFetched(account: Account, image: TelegramMediaWebFile) -> Signal { - return account.postbox.mediaBox.fetchedResource(image.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + return fetchedMediaResource(postbox: account.postbox, reference: .standalone(resource: image.resource), statsCategory: .image) } func chatMessageWebFileCancelInteractiveFetch(account: Account, image: TelegramMediaWebFile) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) } -func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> Signal { - if let closestRepresentation = photo.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) { - let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in +func chatWebpageSnippetFileData(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { + let resourceData = account.postbox.mediaBox.resourceData(resource) + |> map { next in + return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + } + + return Signal { subscriber in + let disposable = DisposableSet() + disposable.add(resourceData.start(next: { data in + subscriber.putNext(data) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + disposable.add(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(resource)).start()) + return disposable + } +} + +func chatWebpageSnippetPhotoData(account: Account, photoReference: ImageMediaReference) -> Signal { + if let closestRepresentation = photoReference.media.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) { + let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) + |> map { next in return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } @@ -1314,7 +1478,7 @@ func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> }, completed: { subscriber.putCompletion() })) - disposable.add(account.postbox.mediaBox.fetchedResource(closestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + disposable.add(fetchedMediaResource(postbox: account.postbox, reference: photoReference.resourceReference(closestRepresentation.resource)).start()) return disposable } } else { @@ -1322,8 +1486,8 @@ func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> } } -func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatWebpageSnippetPhotoData(account: account, photo: photo) +func chatWebpageSnippetFile(account: Account, fileReference: FileMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatWebpageSnippetFileData(account: account, fileReference: fileReference, resource: representation.resource) return signal |> map { fullSizeData in return { arguments in @@ -1365,15 +1529,58 @@ func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage) -> Sig } } -func chatMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return mediaGridMessageVideo(postbox: postbox, video: video) +func chatWebpageSnippetPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatWebpageSnippetPhotoData(account: account, photoReference: photoReference) + + return signal |> map { fullSizeData in + return { arguments in + var fullSizeImage: CGImage? + var imageOrientation: UIImageOrientation = .up + if let fullSizeData = fullSizeData { + let options = NSMutableDictionary() + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image + } + } + + if let fullSizeImage = fullSizeImage { + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) + let drawingRect = arguments.drawingRect + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height { + c.fill(arguments.drawingRect) + } + + c.interpolationQuality = .medium + drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) + } + + addCorners(context, arguments: arguments) + + return context + } else { + return nil + } + } + } } -private func chatSecretMessageVideoData(account: Account, file: TelegramMediaFile) -> Signal { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { +func chatMessageVideo(postbox: Postbox, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return mediaGridMessageVideo(postbox: postbox, videoReference: videoReference) +} + +private func chatSecretMessageVideoData(account: Account, fileReference: FileMediaReference) -> Signal { + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)) + let fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -1392,10 +1599,11 @@ private func chatSecretMessageVideoData(account: Account, file: TelegramMediaFil } } -func chatSecretMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatSecretMessageVideoData(account: account, file: video) +func chatSecretMessageVideo(account: Account, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatSecretMessageVideoData(account: account, fileReference: videoReference) - return signal |> map { thumbnailData in + return signal + |> map { thumbnailData in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { @@ -1460,7 +1668,7 @@ func chatSecretMessageVideo(account: Account, video: TelegramMediaFile) -> Signa } } -private func imageOrientationFromSource(_ source: CGImageSource) -> UIImageOrientation { +func imageOrientationFromSource(_ source: CGImageSource) -> UIImageOrientation { if let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) { let dict = properties as NSDictionary if let value = dict.object(forKey: "Orientation") as? NSNumber { @@ -1471,10 +1679,20 @@ private func imageOrientationFromSource(_ source: CGImageSource) -> UIImageOrien return .up } -private func drawImage(context: CGContext, image: CGImage, orientation: UIImageOrientation, in rect: CGRect) { +func drawImage(context: CGContext, image: CGImage, orientation: UIImageOrientation, in rect: CGRect) { var restore = true var drawRect = rect switch orientation { + case .right: + context.saveGState() + context.translateBy(x: rect.midX, y: rect.midY) + context.rotate(by: -CGFloat.pi / 2.0) + context.translateBy(x: -rect.midX, y: -rect.midY) + var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) + t = t.rotated(by: -CGFloat.pi / 2.0) + t = t.translatedBy(x: -rect.midX, y: -rect.midY) + + drawRect = rect.applying(t) case .leftMirrored: context.saveGState() context.translateBy(x: rect.midX, y: rect.midY) @@ -1494,15 +1712,16 @@ private func drawImage(context: CGContext, image: CGImage, orientation: UIImageO } } -func chatMessageImageFile(account: Account, file: TelegramMediaFile, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +func chatMessageImageFile(account: Account, fileReference: FileMediaReference, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal<(Data?, String?, Bool), NoError> if thumbnail { - signal = chatMessageImageFileThumbnailDatas(account: account, file: file) + signal = chatMessageImageFileThumbnailDatas(account: account, fileReference: fileReference) } else { - signal = chatMessageFileDatas(account: account, file: file, progressive: false) + signal = chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false) } - return signal |> map { (thumbnailData, fullSizePath, fullSizeComplete) in + return signal + |> map { (thumbnailData, fullSizePath, fullSizeComplete) in return { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -1582,17 +1801,19 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, thumbnail: } } -private func avatarGalleryPhotoDatas(account: Account, representations: [TelegramMediaImageRepresentation], autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(representations), let largestRepresentation = largestImageRepresentation(representations) { +private func avatarGalleryPhotoDatas(account: Account, representations: [(TelegramMediaImageRepresentation, MediaResourceReference)], autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.0 })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.0 })), let smallestIndex = representations.index(where: { $0.0 == smallestRepresentation }), let largestIndex = representations.index(where: { $0.0 == largestRepresentation }) { let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - let fetchedFullSize = account.postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = fetchedMediaResource(postbox: account.postbox, reference: representations[smallestIndex].1) + let fetchedFullSize = fetchedMediaResource(postbox: account.postbox, reference: representations[largestIndex].1) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -1634,7 +1855,7 @@ private func avatarGalleryPhotoDatas(account: Account, representations: [Telegra } } } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } |> filter({ $0.0 != nil || $0.1 != nil }) return signal } else { @@ -1642,10 +1863,11 @@ private func avatarGalleryPhotoDatas(account: Account, representations: [Telegra } } -func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaImageRepresentation], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +func chatAvatarGalleryPhoto(account: Account, representations: [(TelegramMediaImageRepresentation, MediaResourceReference)], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return signal + |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -1772,7 +1994,7 @@ func settingsBuiltinWallpaperImage(account: Account) -> Signal<(TransformImageAr func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { return Signal { subscriber in - let fetchedDisposable = account.postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let fetchedDisposable = account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start() let dataDisposable = account.postbox.mediaBox.resourceData(resource).start(next: { next in if next.size != 0 { subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) @@ -1858,60 +2080,60 @@ func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return account.postbox.mediaBox.resourceData(file.resource) - |> map { fullSizeData in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) - - var fullSizeImage: CGImage? - var imageOrientation: UIImageOrientation = .up - if fullSizeData.complete { - let options = NSMutableDictionary() - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizeData.path) as CFURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - imageOrientation = imageOrientationFromSource(imageSource) - fullSizeImage = image - } - - if let fullSizeImage = fullSizeImage { - let drawingRect = arguments.drawingRect - var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) - if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.width = arguments.boundingSize.width - } - if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.height = arguments.boundingSize.height - } - - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - context.withFlippedContext { c in - c.setBlendMode(.copy) - if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - c.fill(arguments.drawingRect) - } - - c.setBlendMode(.copy) - - c.interpolationQuality = .medium - drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) - - c.setBlendMode(.normal) - } - } else { - context.withFlippedContext { c in - c.setBlendMode(.copy) - c.setFillColor(UIColor.white.cgColor) - c.fill(arguments.drawingRect) - - c.setBlendMode(.normal) - } - } + |> map { fullSizeData in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var fullSizeImage: CGImage? + var imageOrientation: UIImageOrientation = .up + if fullSizeData.complete { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizeData.path) as CFURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image } - addCorners(context, arguments: arguments) - - return context + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + + c.interpolationQuality = .medium + drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) + + c.setBlendMode(.normal) + } + } else { + context.withFlippedContext { c in + c.setBlendMode(.copy) + c.setFillColor(UIColor.white.cgColor) + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + } + } } + + addCorners(context, arguments: arguments) + + return context + } } } @@ -1925,7 +2147,7 @@ private func albumArtThumbnailData(postbox: Postbox, thumbnail: MediaResource) - let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData)) } else { - let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, parameters: nil) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -1961,8 +2183,8 @@ private func albumArtFullSizeDatas(postbox: Postbox, thumbnail: MediaResource, f let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((nil, loadedData, true)) } else { - let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - let fetchedFullSize = postbox.mediaBox.fetchedResource(fullSize, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let fetchedThumbnail = postbox.mediaBox.fetchedResource(thumbnail, parameters: nil) + let fetchedFullSize = postbox.mediaBox.fetchedResource(fullSize, parameters: nil) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -2132,7 +2354,7 @@ func securePhoto(account: Account, resource: TelegramMediaResource, accessContex func securePhotoInternal(account: Account, resource: TelegramMediaResource, accessContext: SecureIdAccessContext) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal = Signal { subscriber in - let fetched = account.postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let fetched = account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start() let data = account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)).start(next: { next in subscriber.putNext(next) }, completed: { diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index b0e1f27c97..69861b7132 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -17,6 +17,7 @@ enum PresentationResourceKey: Int32 { case navigationCallIcon case navigationShareIcon case navigationSearchIcon + case navigationCompactSearchIcon case navigationAddIcon case navigationPlayerCloseButton @@ -136,6 +137,7 @@ enum PresentationResourceKey: Int32 { case chatInputPanelVoiceActiveButtonImage case chatInputPanelVideoActiveButtonImage case chatInputPanelAttachmentButtonImage + case chatInputPanelEditAttachmentButtonImage case chatInputPanelMediaRecordingDotImage case chatInputPanelMediaRecordingCancelArrowImage case chatInputTextFieldStickersImage @@ -179,6 +181,7 @@ enum PresentationResourceKey: Int32 { case sharedMediaFileDownloadStartIcon case sharedMediaFileDownloadPauseIcon + case sharedMediaInstantViewIcon case chatInfoCallButtonImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index b7eb96a629..c20ae51e07 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -498,6 +498,82 @@ struct PresentationResourcesChat { }) } + /* + + + + + Created with Sketch. + + + + + + + + + + + + + + Created with Sketch. + + + + + + + + */ + + static func chatInputPanelEditAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelEditAttachmentButtonImage.rawValue, { theme in + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: theme.chat.inputPanel.panelControlColor) { + return generateImage(image.size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + let imageRect = CGRect(origin: CGPoint(), size: image.size) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(image.cgImage!, in: imageRect) + context.restoreGState() + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + let circleSide: CGFloat = 15.0 + let circleRect = CGRect(origin: CGPoint(x: size.width - circleSide - 1.0, y: size.height - circleSide - 1.0), size: CGSize(width: circleSide, height: circleSide)) + context.fillEllipse(in: circleRect) + + context.translateBy(x: circleRect.minX, y: circleRect.minY) + + context.saveGState() + context.translateBy(x: -1.0, y: -5.0) + let _ = try? drawSvgPath(context, path: "M6,0.909090909 L6,14 L13,7.45454545 Z ") + context.restoreGState() + + context.setBlendMode(.normal) + context.setFillColor(theme.chat.inputPanel.panelControlAccentColor.cgColor) + let _ = try? drawSvgPath(context, path: "M6.675,-1.99172619 L11.3939989,2.72727273 L6.675,7.44627164 L6.675,-1.99172619 Z ") + + context.setStrokeColor(theme.chat.inputPanel.panelControlAccentColor.cgColor) + context.setLineWidth(1.65) + context.setLineCap(.round) + + context.saveGState() + context.translateBy(x: 7.5, y: 7.5) + context.scaleBy(x: -1.0, y: -1.0) + context.translateBy(x: -7.5, y: -7.5) + let _ = try? drawSvgPath(context, path: "M7.5,12.2727273 C10.1359045,12.2727273 12.2727273,10.1359045 12.2727273,7.5 C12.2727273,4.86409551 10.1359045,2.72727273 7.5,2.72727273 C4.86409551,2.72727273 2.72727273,4.86409551 2.72727273,7.5 S ") + context.restoreGState() + }) + } else { + return nil + } + }) + } + static func chatInputPanelExpandButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelExpandButtonImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconExpandInput"), color: theme.chat.inputPanel.panelControlColor) @@ -621,6 +697,21 @@ struct PresentationResourcesChat { }) } + static func sharedMediaInstantViewIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.sharedMediaInstantViewIcon.rawValue, { theme in + return generateImage(CGSize(width: 9.0, height: 12.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(theme.list.itemAccentColor.cgColor) + + context.scaleBy(x: 0.3333, y: 0.3333) + context.translateBy(x: -45.0, y: -44.0) + + let _ = try? drawSvgPath(context, path: "M62.4237573,56.242532 L68.6008574,45.3138164 C69.9641724,42.9017976 69.2490118,42.2787106 67.0133901,43.9046172 L50.6790537,55.7841346 C49.7933206,56.4283042 49.7678266,57.5101414 50.6393204,58.18797 L57.6989251,63.6787736 L51.521825,74.6074892 C50.15851,77.019508 50.8736706,77.642595 53.1092923,76.0166884 L69.4436287,64.137171 C70.3293618,63.4930014 70.3548559,62.4111642 69.483362,61.7333356 L62.4237573,56.242532 Z ") + }) + }) + } + static func chatInfoCallButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInfoCallButtonImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.list.itemAccentColor) diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index d80157dc92..2a5e4ef56c 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -82,6 +82,18 @@ struct PresentationResourcesRootController { }) } + static func navigationCompactSearchIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationCompactSearchIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor).flatMap({ image in + let factor: CGFloat = 0.8 + let size = CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)) + return generateImage(size, rotatedContext: { size, context in + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }) + }) + }) + } + static func navigationAddIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationAddIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index 50e7e3b8ce..3e7d75a40c 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -9,7 +9,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageId: MessageId - private var previousMedia: Media? + private var previousMediaReference: AnyMediaReference? let closeButton: ASButtonNode let lineNode: ASImageNode @@ -59,7 +59,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.imageNode) self.messageDisposable.set((account.postbox.messageAtId(messageId) - |> deliverOnMainQueue).start(next: { [weak self] message in + |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { var authorName = "" var text = "" @@ -70,19 +70,19 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { (text, _) = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId) } - var updatedMedia: Media? + var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? var isRoundImage = false if let message = message, !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { - updatedMedia = image + updatedMediaReference = .message(message: MessageReference(message), media: image) if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions } break } else if let file = media as? TelegramMediaFile { - updatedMedia = file + updatedMediaReference = .message(message: MessageReference(message), media: file) isRoundImage = file.isInstantVideo if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { imageDimensions = representation.dimensions @@ -107,24 +107,23 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = strongSelf.previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (strongSelf.previousMedia != nil) { + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = strongSelf.previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(previousMediaReference.media) + } else if (updatedMediaReference != nil) != (strongSelf.previousMediaReference != nil) { mediaUpdated = true } - strongSelf.previousMedia = updatedMedia + strongSelf.previousMediaReference = updatedMediaReference var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { - if let updatedMedia = updatedMedia, imageDimensions != nil { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) - } else if let file = updatedMedia as? TelegramMediaFile { - if file.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) - } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + updateImageSignal = chatMessagePhotoThumbnail(account: account, photoReference: imageReference) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: account, fileReference: fileReference) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: account, fileReference: fileReference, representation: iconImageRepresentation) } } } else { diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift index 11dcae0003..a9d90a88dc 100644 --- a/TelegramUI/SaveToCameraRoll.swift +++ b/TelegramUI/SaveToCameraRoll.swift @@ -4,20 +4,21 @@ import SwiftSignalKit import Postbox import TelegramCore import Photos +import Display -func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { +func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal { var resource: MediaResource? var isImage = true - if let image = media as? TelegramMediaImage { + if let image = mediaReference.media as? TelegramMediaImage { if let representation = largestImageRepresentation(image.representations) { resource = representation.resource } - } else if let file = media as? TelegramMediaFile { + } else if let file = mediaReference.media as? TelegramMediaFile { resource = file.resource if file.isVideo { isImage = false } - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + } else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let image = content.image { if let representation = largestImageRepresentation(image.representations) { resource = representation.resource @@ -32,7 +33,7 @@ func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { if let resource = resource { let fetchedData: Signal = Signal { subscriber in - let fetched = postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let fetched = fetchedMediaResource(postbox: postbox, reference: mediaReference.resourceReference(resource)).start() let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) }, completed: { @@ -44,9 +45,17 @@ func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { } } return fetchedData - |> mapToSignal { data -> Signal in - if data.complete { - return Signal { subscriber in + |> mapToSignal { data -> Signal in + if data.complete { + return Signal { subscriber in + authorizeDeviceAccess(to: .mediaLibrary(.save), presentationData: applicationContext.currentPresentationData.with { $0 }, present: { c, a in + applicationContext.presentGlobalController(c, a) + }, openSettings: applicationContext.applicationBindings.openSettings, { authorized in + if !authorized { + subscriber.putCompletion() + return + } + let tempVideoPath = NSTemporaryDirectory() + "\(arc4random64()).mp4" PHPhotoLibrary.shared().performChanges({ if isImage { @@ -72,14 +81,18 @@ func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { subscriber.putNext(Void()) subscriber.putCompletion() }) - - return ActionDisposable { - } + }) + + return ActionDisposable { } - } else { - return .complete() } - } |> take(1) |> mapToSignal { _ -> Signal in return .complete() } + } else { + return .complete() + } + } + |> take(1) + |> mapToSignal { _ -> Signal in return .complete() + } } else { return .complete() } diff --git a/TelegramUI/SearchPeerMembers.swift b/TelegramUI/SearchPeerMembers.swift new file mode 100644 index 0000000000..a05f03be61 --- /dev/null +++ b/TelegramUI/SearchPeerMembers.swift @@ -0,0 +1,55 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +func searchPeerMembers(account: Account, peerId: PeerId, query: String) -> Signal<[Peer], NoError> { + if peerId.namespace == Namespaces.Peer.CloudChannel && !query.isEmpty { + return account.postbox.transaction { transaction -> CachedChannelData? in + return transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData + } + |> mapToSignal { cachedData -> Signal<[Peer], NoError> in + if let cachedData = cachedData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 64 { + return Signal { subscriber in + let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in + if case .ready = state.loadingState { + let normalizedQuery = query.lowercased() + subscriber.putNext(state.list.compactMap { participant -> Peer? in + if normalizedQuery.isEmpty { + return participant.peer + } + + if participant.peer.indexName.matchesByTokens(normalizedQuery) { + return participant.peer + } + if let addressName = participant.peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return participant.peer + } + + return nil + }) + } + }) + + return ActionDisposable { + disposable.dispose() + } + } |> runOn(Queue.mainQueue()) + } + + return Signal { subscriber in + let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, searchQuery: query, updated: { state in + if case .ready = state.loadingState { + subscriber.putNext(state.list.map { $0.peer }) + } + }) + + return ActionDisposable { + disposable.dispose() + } + } |> runOn(Queue.mainQueue()) + } + } else { + return searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peerId, query: query) + } +} diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index daed2def35..5e59c63f73 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -64,7 +64,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { let buttonSize = self.button.measure(CGSize(width: width - 10.0, height: 100.0)) - let panelHeight: CGFloat = 47.0 + let panelHeight: CGFloat = 45.0 self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index 104a162e27..8948aceb03 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -166,8 +166,11 @@ public final class SecretMediaPreviewController: ViewController { } }) - self.screenCaptureEventsDisposable = screenCaptureEvents().start(next: { _ in - let _ = addSecretChatMessageScreenshot(account: account, peerId: messageId.peerId).start() + self.screenCaptureEventsDisposable = (screenCaptureEvents() + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let _ = self { + let _ = addSecretChatMessageScreenshot(account: account, peerId: messageId.peerId).start() + } }) } @@ -181,6 +184,7 @@ public final class SecretMediaPreviewController: ViewController { if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex { self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) } + self.screenCaptureEventsDisposable?.dispose() } @objc func donePressed() { diff --git a/TelegramUI/SecureIdDocumentImageGalleryItem.swift b/TelegramUI/SecureIdDocumentImageGalleryItem.swift index 839046054f..dbc0971b52 100644 --- a/TelegramUI/SecureIdDocumentImageGalleryItem.swift +++ b/TelegramUI/SecureIdDocumentImageGalleryItem.swift @@ -126,7 +126,7 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { return value.1 }) self.zoomableContent = (displaySize, self.imageNode) - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start()) + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start()) } self.accountAndMedia = (account, context, resource) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 9bcf13d9d7..e89f48fb9b 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -65,7 +65,7 @@ private enum SettingsEntry: ItemListNodeEntry { case savedMessages(PresentationTheme, UIImage?, String) case recentCalls(PresentationTheme, UIImage?, String) - case stickers(PresentationTheme, UIImage?, String) + case stickers(PresentationTheme, UIImage?, String, String) case notificationsAndSounds(PresentationTheme, UIImage?, String) case privacyAndSecurity(PresentationTheme, UIImage?, String) @@ -193,8 +193,8 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } - case let .stickers(lhsTheme, lhsImage, lhsText): - if case let .stickers(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { + case let .stickers(lhsTheme, lhsImage, lhsText, lhsValue): + if case let .stickers(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -283,8 +283,8 @@ private enum SettingsEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openRecentCalls() }) - case let .stickers(theme, image, text): - return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .stickers(theme, image, text, value): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(installedStickerPacksController(account: arguments.account, mode: .general)) }) case let .notificationsAndSounds(theme, image, text): @@ -342,7 +342,7 @@ private struct SettingsState: Equatable { } } -private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings) -> [SettingsEntry] { +private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, unreadTrendingStickerPacks: Int) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { @@ -356,12 +356,23 @@ private func settingsEntries(presentationData: PresentationData, state: Settings } if !proxySettings.servers.isEmpty { - entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, presentationData.strings.Settings_Proxy, proxySettings.enabled ? presentationData.strings.UserInfo_NotificationsEnabled : presentationData.strings.Settings_ProxyDisabled)) + let valueString: String + if proxySettings.enabled, let activeServer = proxySettings.activeServer { + switch activeServer.connection { + case .mtp: + valueString = presentationData.strings.SocksProxySetup_ProxyTelegram + case .socks5: + valueString = presentationData.strings.SocksProxySetup_ProxySocks5 + } + } else { + valueString = presentationData.strings.Settings_ProxyDisabled + } + entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, presentationData.strings.Settings_Proxy, valueString)) } entries.append(.savedMessages(presentationData.theme, SettingsItemIcons.savedMessages, presentationData.strings.Settings_SavedMessages)) entries.append(.recentCalls(presentationData.theme, SettingsItemIcons.recentCalls, presentationData.strings.CallSettings_RecentCalls)) - entries.append(.stickers(presentationData.theme, SettingsItemIcons.stickers, presentationData.strings.ChatSettings_Stickers)) + entries.append(.stickers(presentationData.theme, SettingsItemIcons.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)")) entries.append(.notificationsAndSounds(presentationData.theme, SettingsItemIcons.notifications, presentationData.strings.Settings_NotificationsAndSounds)) entries.append(.privacyAndSecurity(presentationData.theme, SettingsItemIcons.security, presentationData.strings.Settings_PrivacySettings)) @@ -590,8 +601,8 @@ public func settingsController(account: Account, accountManager: AccountManager) let peerView = account.viewTracker.peerView(account.peerId) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings])) - |> map { presentationData, state, view, preferences -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]), account.viewTracker.featuredStickerPacks()) + |> map { presentationData, state, view, preferences, featuredStickerPacks -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let proxySettings: ProxySettings if let value = preferences.values[PreferencesKeys.proxySettings] as? ProxySettings { proxySettings = value @@ -607,7 +618,15 @@ public func settingsController(account: Account, accountManager: AccountManager) }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Settings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, proxySettings: proxySettings), style: .blocks) + + var unreadTrendingStickerPacks = 0 + for item in featuredStickerPacks { + if item.unread { + unreadTrendingStickerPacks += 1 + } + } + + let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/SettingsThemeWallpaperNode.swift b/TelegramUI/SettingsThemeWallpaperNode.swift index 4487e572ef..37aee85688 100644 --- a/TelegramUI/SettingsThemeWallpaperNode.swift +++ b/TelegramUI/SettingsThemeWallpaperNode.swift @@ -46,7 +46,9 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { case let .image(representations): self.imageNode.isHidden = false self.backgroundNode.isHidden = true - self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: representations, autoFetchFullSize: true)) + + let convertedRepresentations: [(TelegramMediaImageRepresentation, MediaResourceReference)] = representations.map({ ($0, .wallpaper(resource: $0.resource)) }) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: convertedRepresentations, autoFetchFullSize: true)) let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: largestImageRepresentation(representations)!.dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() } diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 59dd2e9063..2063463676 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -18,6 +18,7 @@ public enum ShareControllerExternalStatus { public enum ShareControllerSubject { case url(String) case text(String) + case quote(text: String, url: String) case messages([Message]) case image([TelegramMediaImageRepresentation]) case mapMedia(TelegramMediaMap) @@ -41,10 +42,10 @@ private enum ExternalShareResourceStatus { case done(MediaResourceData) } -private func collectExternalShareResource(postbox: Postbox, resource: MediaResource, tag: MediaResourceFetchTag) -> Signal { +private func collectExternalShareResource(postbox: Postbox, resourceReference: MediaResourceReference, statsCategory: MediaResourceStatsCategory) -> Signal { return Signal { subscriber in - let fetched = postbox.mediaBox.fetchedResource(resource, tag: tag).start() - let data = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)).start(next: { value in + let fetched = fetchedMediaResource(postbox: postbox, reference: resourceReference, statsCategory: statsCategory).start() + let data = postbox.mediaBox.resourceData(resourceReference.resource, option: .complete(waitUntilFetchStatus: false)).start(next: { value in if value.complete { subscriber.putNext(.done(value)) } else { @@ -67,53 +68,53 @@ private enum ExternalShareItemsState { private struct CollectableExternalShareItem { let url: String? let text: String - let media: Media? + let mediaReference: AnyMediaReference? } private func collectExternalShareItems(postbox: Postbox, collectableItems: [CollectableExternalShareItem]) -> Signal { var signals: [Signal] = [] for item in collectableItems { - if let file = item.media as? TelegramMediaFile { - signals.append(collectExternalShareResource(postbox: postbox, resource: file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) - |> map { next -> ExternalShareItemStatus in - switch next { - case .progress: + if let mediaReference = item.mediaReference, let file = mediaReference.media as? TelegramMediaFile { + signals.append(collectExternalShareResource(postbox: postbox, resourceReference: mediaReference.resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)) + |> map { next -> ExternalShareItemStatus in + switch next { + case .progress: + return .progress + case let .done(data): + let fileName: String + if let value = file.fileName { + fileName = value + } else if file.isVideo { + fileName = "telegram_video.mp4" + } else { + fileName = "file" + } + let randomDirectory = UUID() + let safeFileName = fileName.replacingOccurrences(of: "/", with: "_") + let fileDirectory = NSTemporaryDirectory() + "\(randomDirectory)" + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: fileDirectory), withIntermediateDirectories: true, attributes: nil) + let filePath = fileDirectory + "/\(safeFileName)" + if let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: data.path), to: URL(fileURLWithPath: filePath)) { + return .done(.file(URL(fileURLWithPath: filePath), fileName, file.mimeType)) + } else { return .progress - case let .done(data): - let fileName: String - if let value = file.fileName { - fileName = value - } else if file.isVideo { - fileName = "telegram_video.mp4" - } else { - fileName = "file" - } - let randomDirectory = UUID() - let safeFileName = fileName.replacingOccurrences(of: "/", with: "_") - let fileDirectory = NSTemporaryDirectory() + "\(randomDirectory)" - let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: fileDirectory), withIntermediateDirectories: true, attributes: nil) - let filePath = fileDirectory + "/\(safeFileName)" - if let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: data.path), to: URL(fileURLWithPath: filePath)) { - return .done(.file(URL(fileURLWithPath: filePath), fileName, file.mimeType)) - } else { - return .progress - } - } - }) - } else if let image = item.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { - signals.append(collectExternalShareResource(postbox: postbox, resource: largest.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - |> map { next -> ExternalShareItemStatus in - switch next { - case .progress: + } + } + }) + } else if let mediaReference = item.mediaReference, let image = mediaReference.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { + signals.append(collectExternalShareResource(postbox: postbox, resourceReference: mediaReference.resourceReference(largest.resource), statsCategory: .image) + |> map { next -> ExternalShareItemStatus in + switch next { + case .progress: + return .progress + case let .done(data): + if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: fileData) { + return .done(.image(image)) + } else { return .progress - case let .done(data): - if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: fileData) { - return .done(.image(image)) - } else { - return .progress - } - } - }) + } + } + }) } if let url = item.url, let parsedUrl = URL(string: url) { if signals.isEmpty { @@ -127,25 +128,25 @@ private func collectExternalShareItems(postbox: Postbox, collectableItems: [Coll } } return combineLatest(signals) - |> map { statuses -> ExternalShareItemsState in - var items: [ExternalShareItem] = [] - for status in statuses { - switch status { - case .progress: - return .progress - case let .done(item): - items.append(item) - } + |> map { statuses -> ExternalShareItemsState in + var items: [ExternalShareItem] = [] + for status in statuses { + switch status { + case .progress: + return .progress + case let .done(item): + items.append(item) } - return .done(items) } - |> distinctUntilChanged(isEqual: { lhs, rhs in - if case .progress = lhs, case .progress = rhs { - return true - } else { - return false - } - }) + return .done(items) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if case .progress = lhs, case .progress = rhs { + return true + } else { + return false + } + }) } public final class ShareController: ViewController { @@ -188,6 +189,8 @@ public final class ShareController: ViewController { break case .mapMedia: break + case .quote: + break case let .image(representations): if saveToCameraRoll { self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in @@ -195,12 +198,12 @@ public final class ShareController: ViewController { }) } case let .messages(messages): - if messages.count == 1, let message = messages.first { - if saveToCameraRoll { - self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in - self?.saveToCameraRoll(message: message) - }) - } else if let showInChat = showInChat { + if saveToCameraRoll { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self?.saveToCameraRoll(messages: messages) + }) + } else if messages.count == 1, let message = messages.first { + if let showInChat = showInChat { self.defaultAction = ShareControllerAction(title: self.presentationData.strings.SharedMedia_ViewInChat, action: { [weak self] in self?.controllerNode.cancel?() showInChat(message) @@ -279,6 +282,19 @@ public final class ShareController: ViewController { let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() + case let .quote(string, url): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + } + let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) + attributedText.append(NSAttributedString(string: "\n\n\(url)")) + let entities = generateChatInputTextEntities(attributedText) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + return .complete() case let .image(representations): for peerId in peerIds { var messages: [EnqueueMessage] = [] @@ -325,15 +341,17 @@ public final class ShareController: ViewController { var collectableItems: [CollectableExternalShareItem] = [] switch strongSelf.subject { case let .url(text): - collectableItems.append(CollectableExternalShareItem(url: text, text: "", media: nil)) + collectableItems.append(CollectableExternalShareItem(url: text, text: "", mediaReference: nil)) case let .text(string): - collectableItems.append(CollectableExternalShareItem(url: "", text: string, media: nil)) + collectableItems.append(CollectableExternalShareItem(url: "", text: string, mediaReference: nil)) + case let .quote(text, url): + collectableItems.append(CollectableExternalShareItem(url: "", text: "\"\(text)\"\n\n\(url)", mediaReference: nil)) case let .image(representations): let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil) - collectableItems.append(CollectableExternalShareItem(url: "", text: "", media: media)) + collectableItems.append(CollectableExternalShareItem(url: "", text: "", mediaReference: .standalone(media: media))) case let .mapMedia(media): let latLong = "\(media.latitude),\(media.longitude)" - collectableItems.append(CollectableExternalShareItem(url: "https://maps.apple.com/maps?ll=\(latLong)&q=\(latLong)&t=m", text: "", media: nil)) + collectableItems.append(CollectableExternalShareItem(url: "https://media: maps.apple.com/maps?ll=\(latLong)&q=\(latLong)&t=m", text: "", mediaReference: nil)) case let .messages(messages): for message in messages { var url: String? @@ -360,7 +378,7 @@ public final class ShareController: ViewController { url = "https://t.me/\(addressName)/\(message.id.id)" } } - collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, media: selectedMedia)) + collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) }))) } case .fromExternal: break @@ -435,14 +453,22 @@ public final class ShareController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } - private func saveToCameraRoll(message: Message) { - if let media = message.media.first { - self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(postbox: self.account.postbox, media: media)) + private func saveToCameraRoll(messages: [Message]) { + let postbox = self.account.postbox + let signals: [Signal] = messages.compactMap { message -> Signal? in + if let media = message.media.first { + return TelegramUI.saveToCameraRoll(applicationContext: self.account.telegramApplicationContext, postbox: postbox, mediaReference: .message(message: MessageReference(message), media: media)) + } else { + return nil + } + } + if !signals.isEmpty { + self.controllerNode.transitionToProgress(signal: combineLatest(signals) |> mapToSignal { _ -> Signal in return .complete() }) } } private func saveToCameraRoll(image: [TelegramMediaImageRepresentation]) { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image, reference: nil) - self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(postbox: self.account.postbox, media: media)) + self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(applicationContext: self.account.telegramApplicationContext, postbox: self.account.postbox, mediaReference: .standalone(media: media))) } } diff --git a/TelegramUI/SharedMediaPlayer.swift b/TelegramUI/SharedMediaPlayer.swift index c2546daa51..ad7b64dd9c 100644 --- a/TelegramUI/SharedMediaPlayer.swift +++ b/TelegramUI/SharedMediaPlayer.swift @@ -32,13 +32,16 @@ enum SharedMediaPlaybackDataType { } enum SharedMediaPlaybackDataSource: Equatable { - case telegramFile(TelegramMediaFile) + case telegramFile(FileMediaReference) static func ==(lhs: SharedMediaPlaybackDataSource, rhs: SharedMediaPlaybackDataSource) -> Bool { switch lhs { - case let .telegramFile(lhsFile): - if case let .telegramFile(rhsFile) = rhs { - return lhsFile.isEqual(rhsFile) + case let .telegramFile(lhsFileReference): + if case let .telegramFile(rhsFileReference) = rhs { + if !lhsFileReference.media.isEqual(rhsFileReference.media) { + return false + } + return true } else { return false } @@ -75,7 +78,7 @@ struct SharedMediaPlaybackAlbumArt: Equatable { enum SharedMediaPlaybackDisplayData: Equatable { case music(title: String?, performer: String?, albumArt: SharedMediaPlaybackAlbumArt?) case voice(author: Peer?, peer: Peer?) - case instantVideo(author: Peer?, peer: Peer?) + case instantVideo(author: Peer?, peer: Peer?, timestamp: Int32) static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> Bool { switch lhs { @@ -91,8 +94,8 @@ enum SharedMediaPlaybackDisplayData: Equatable { } else { return false } - case let .instantVideo(lhsAuthor, lhsPeer): - if case let .instantVideo(rhsAuthor, rhsPeer) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer) { + case let .instantVideo(lhsAuthor, lhsPeer, lhsTimestamp): + if case let .instantVideo(rhsAuthor, rhsPeer, rhsTimestamp) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer), lhsTimestamp == rhsTimestamp { return true } else { return false @@ -423,14 +426,14 @@ final class SharedMediaPlayer { switch playbackData.type { case .voice, .music: switch playbackData.source { - case let .telegramFile(file): - strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.postbox, resource: file.resource, streamable: playbackData.type == .music, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity)) + case let .telegramFile(fileReference): + strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.postbox, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity)) } case .instantVideo: if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { - case let .telegramFile(file): - let videoNode = OverlayInstantVideoNode(postbox: strongSelf.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.id, item.message.stableId, file.fileId), file: file, streamVideo: false, enableSound: false), close: { [weak mediaManager] in + case let .telegramFile(fileReference): + let videoNode = OverlayInstantVideoNode(postbox: strongSelf.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.id, item.message.stableId, fileReference.media.fileId), fileReference: fileReference, streamVideo: false, enableSound: false), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice) }) strongSelf.playbackItem = .instantVideo(videoNode) diff --git a/TelegramUI/SoftwareVideoLayerFrameManager.swift b/TelegramUI/SoftwareVideoLayerFrameManager.swift index bb35d73c81..967e487a91 100644 --- a/TelegramUI/SoftwareVideoLayerFrameManager.swift +++ b/TelegramUI/SoftwareVideoLayerFrameManager.swift @@ -28,7 +28,7 @@ final class SoftwareVideoLayerFrameManager { private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? - init(account: Account, resource: MediaResource, layerHolder: SampleBufferLayer) { + init(account: Account, fileReference: FileMediaReference, resource: MediaResource, layerHolder: SampleBufferLayer) { nextWorker += 1 self.account = account self.resource = resource @@ -36,7 +36,7 @@ final class SoftwareVideoLayerFrameManager { self.layerHolder = layerHolder layerHolder.layer.videoGravity = .resizeAspectFill layerHolder.layer.masksToBounds = true - self.fetchDisposable = account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start() + self.fetchDisposable = fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(resource)).start() } deinit { diff --git a/TelegramUI/SoftwareVideoThumbnailLayer.swift b/TelegramUI/SoftwareVideoThumbnailLayer.swift index 88db5a3f5b..e3c038ffc1 100644 --- a/TelegramUI/SoftwareVideoThumbnailLayer.swift +++ b/TelegramUI/SoftwareVideoThumbnailLayer.swift @@ -15,16 +15,15 @@ final class SoftwareVideoThumbnailLayer: CALayer { } } - init(account: Account, file: TelegramMediaFile) { + init(account: Account, fileReference: FileMediaReference) { super.init() self.backgroundColor = UIColor.white.cgColor self.contentsGravity = "resizeAspectFill" self.masksToBounds = true - if let dimensions = file.dimensions { - self.disposable = (mediaGridMessageVideo(postbox: account.postbox, video: file) |> deliverOn(account.graphicsThreadPool)).start(next: { [weak self] transform in - + if let dimensions = fileReference.media.dimensions { + self.disposable = (mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference) |> deliverOn(account.graphicsThreadPool)).start(next: { [weak self] transform in var boundingSize = dimensions.aspectFilled(CGSize(width: 93.0, height: 93.0)) let imageSize = boundingSize boundingSize.width = min(200.0, boundingSize.width) diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index eaf24df06e..c3a1d521f5 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -29,7 +29,7 @@ final class StickerPackPreviewController: ViewController { private let openMentionDisposable = MetaDisposable() - var sendSticker: ((TelegramMediaFile) -> Void)? { + var sendSticker: ((FileMediaReference) -> Void)? { didSet { if self.isNodeLoaded { if let sendSticker = self.sendSticker { diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 1420f70ef5..a30113e4da 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -33,7 +33,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? - var sendSticker: ((TelegramMediaFile) -> Void)? + var sendSticker: ((FileMediaReference) -> Void)? let ready = Promise() private var didSetReady = false @@ -191,7 +191,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol if strongSelf.sendSticker != nil { menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { - strongSelf.sendSticker?(item.file) + strongSelf.sendSticker?(.standalone(media: item.file)) } })) } diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index c08564306b..ab5e2eb4b4 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -101,7 +101,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .black, paragraphAlignment: .right) if let dimensions = stickerItem.file.dimensions { self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file)).start()) self.currentState = (account, stickerItem, dimensions) self.setNeedsLayout() diff --git a/TelegramUI/StickerPaneSearchBarNode.swift b/TelegramUI/StickerPaneSearchBarNode.swift index 37bd4ad851..c74acc2f25 100644 --- a/TelegramUI/StickerPaneSearchBarNode.swift +++ b/TelegramUI/StickerPaneSearchBarNode.swift @@ -187,7 +187,7 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + //self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true diff --git a/TelegramUI/StickerPaneSearchContainerNode.swift b/TelegramUI/StickerPaneSearchContainerNode.swift index 1b6af6de79..494ed28ce6 100644 --- a/TelegramUI/StickerPaneSearchContainerNode.swift +++ b/TelegramUI/StickerPaneSearchContainerNode.swift @@ -11,10 +11,10 @@ import TelegramUIPrivateModule final class StickerPaneSearchInteraction { let open: (StickerPackCollectionInfo) -> Void let install: (StickerPackCollectionInfo) -> Void - let sendSticker: (TelegramMediaFile) -> Void + let sendSticker: (FileMediaReference) -> Void let getItemIsPreviewed: (StickerPackItem) -> Bool - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (FileMediaReference) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.open = open self.install = install self.sendSticker = sendSticker @@ -92,7 +92,7 @@ private enum StickerSearchEntry: Identifiable, Comparable { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { - interaction.sendSticker(stickerItem.file) + interaction.sendSticker(.standalone(media: stickerItem.file)) }) case let .global(_, info, topItems, installed): return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, installed: installed, unread: false, open: { @@ -279,6 +279,7 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { let local = searchStickerSets(postbox: account.postbox, query: text) let remote = searchStickerSetsRemotely(network: account.network, query: text) + |> delay(0.2, queue: Queue.mainQueue()) let packs = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in var localResult = result @@ -339,10 +340,8 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { } } - if final { + if final || !entries.isEmpty { strongSelf.notFoundNode.isHidden = !entries.isEmpty - } else { - strongSelf.notFoundNode.isHidden = true } } else { let _ = currentRemotePacks.swap(nil) @@ -402,9 +401,6 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { } func animateIn(from placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition) { - self.backgroundNode.alpha = 0.0 - transition.updateAlpha(node: self.backgroundNode, alpha: 1.0, completion: { _ in - }) self.gridNode.alpha = 0.0 transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in }) @@ -413,13 +409,28 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { }) switch transition { case let .animated(duration, curve): + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0) self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction) + let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) + if let size = self.validLayout { + let verticalOrigin = placeholderFrame.minY - 4.0 + let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))) + self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction) + self.trendingPane.layer.animatePosition(from: CGPoint(x: 0.0, y: initialBackgroundFrame.minY - self.backgroundNode.frame.minY), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true) + } case .immediate: break } } func animateOut(to placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + if case let .animated(duration, curve) = transition { + if let size = self.validLayout { + let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) + let verticalOrigin = placeholderFrame.minY - 4.0 + self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false) + } + } self.searchBar.transitionOut(to: placeholder, transition: transition, completion: { completion() }) diff --git a/TelegramUI/StickerPaneSearchGlobaltem.swift b/TelegramUI/StickerPaneSearchGlobaltem.swift index 4cd680ab06..6cee2eda96 100644 --- a/TelegramUI/StickerPaneSearchGlobaltem.swift +++ b/TelegramUI/StickerPaneSearchGlobaltem.swift @@ -261,7 +261,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { if file.fileId != node.file?.fileId { node.file = file node.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) - node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: file).start()) + node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file)).start()) } if let dimensions = file.dimensions { let imageSize = dimensions.aspectFitted(itemSize) diff --git a/TelegramUI/StickerPaneSearchStickerItem.swift b/TelegramUI/StickerPaneSearchStickerItem.swift index 54362abda1..bf5ef14486 100644 --- a/TelegramUI/StickerPaneSearchStickerItem.swift +++ b/TelegramUI/StickerPaneSearchStickerItem.swift @@ -133,7 +133,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { if let dimensions = stickerItem.file.dimensions { self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file)).start()) self.currentState = (account, stickerItem, dimensions) self.setNeedsLayout() diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 77dec1ad94..d10ce2b139 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -49,7 +49,7 @@ private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, if fetched { return Signal { subscriber in - let fetch = account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start() + let fetch = fetchedMediaResource(postbox: account.postbox, reference: stickerPackFileReference(file).resourceReference(file.resource)).start() let disposable = (fullSizeData |> map { (data, complete) -> (Data?, Data?, Bool) in return (nil, data, complete) }).start(next: { next in diff --git a/TelegramUI/StringForMessageTimestampStatus.swift b/TelegramUI/StringForMessageTimestampStatus.swift index f42b319d36..0b491cc716 100644 --- a/TelegramUI/StringForMessageTimestampStatus.swift +++ b/TelegramUI/StringForMessageTimestampStatus.swift @@ -2,7 +2,12 @@ import Foundation import Postbox import TelegramCore -func stringForMessageTimestampStatus(message: Message, timeFormat: PresentationTimeFormat, strings: PresentationStrings) -> String { +enum MessageTimestampStatusFormat { + case regular + case minimal +} + +func stringForMessageTimestampStatus(message: Message, timeFormat: PresentationTimeFormat, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular) -> String { var dateText = stringForMessageTimestamp(timestamp: message.timestamp, timeFormat: timeFormat) var authorTitle: String? @@ -21,8 +26,10 @@ func stringForMessageTimestampStatus(message: Message, timeFormat: PresentationT } } - if let authorTitle = authorTitle, !authorTitle.isEmpty { - dateText = "\(authorTitle), \(dateText)" + if case .regular = format { + if let authorTitle = authorTitle, !authorTitle.isEmpty { + dateText = "\(authorTitle), \(dateText)" + } } return dateText diff --git a/TelegramUI/SystemVideoContent.swift b/TelegramUI/SystemVideoContent.swift index 953ab0118f..39cdbd5324 100644 --- a/TelegramUI/SystemVideoContent.swift +++ b/TelegramUI/SystemVideoContent.swift @@ -10,20 +10,20 @@ import LegacyComponents final class SystemVideoContent: UniversalVideoContent { let id: AnyHashable let url: String - let image: TelegramMediaImage + let imageReference: ImageMediaReference let dimensions: CGSize let duration: Int32 - init(url: String, image: TelegramMediaImage, dimensions: CGSize, duration: Int32) { + init(url: String, imageReference: ImageMediaReference, dimensions: CGSize, duration: Int32) { self.id = AnyHashable(url) self.url = url - self.image = image + self.imageReference = imageReference self.dimensions = dimensions self.duration = duration } func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, url: self.url, image: self.image, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) + return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } @@ -71,7 +71,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent private var didPlayToEndTimeObserver: NSObjectProtocol? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, url: String, image: TelegramMediaImage, intrinsicDimensions: CGSize, approximateDuration: Int32) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, url: String, imageReference: ImageMediaReference, intrinsicDimensions: CGSize, approximateDuration: Int32) { self.audioSessionManager = audioSessionManager self.url = url @@ -95,7 +95,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent super.init() - self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, photo: image)) + self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, photoReference: imageReference)) self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 7133af9f8c..9ebf3fcb8a 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -3,6 +3,7 @@ import SwiftSignalKit import UIKit import Postbox import TelegramCore +import Display public final class TelegramApplicationOpenUrlCompletion { public let completion: (Bool) -> Void @@ -23,8 +24,9 @@ public final class TelegramApplicationBindings { public let applicationIsActive: Signal public let clearMessageNotifications: ([MessageId]) -> Void public let pushIdleTimerExtension: () -> Disposable + public let openSettings: () -> Void - public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable) { + public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void) { self.isMainApp = isMainApp self.openUrl = openUrl self.openUniversalUrl = openUniversalUrl @@ -35,6 +37,7 @@ public final class TelegramApplicationBindings { self.applicationIsActive = applicationIsActive self.clearMessageNotifications = clearMessageNotifications self.pushIdleTimerExtension = pushIdleTimerExtension + self.openSettings = openSettings } } @@ -74,6 +77,9 @@ public final class TelegramApplicationContext { private let presentationDataDisposable = MetaDisposable() private let automaticMediaDownloadSettingsDisposable = MetaDisposable() + public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in + } + public var navigateToCurrentCall: (() -> Void)? public var hasOngoingCall: Signal? private var immediateHasOngoingCallValue = Atomic(value: false) diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index 2936ed17c9..42844f8a06 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -56,6 +56,9 @@ public class TelegramController: ViewController { private var dismissingPanel: ASDisplayNode? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + override public var navigationHeight: CGFloat { var height = super.navigationHeight if let _ = self.mediaAccessoryPanel { @@ -69,6 +72,7 @@ public class TelegramController: ViewController { init(account: Account, navigationBarPresentationData: NavigationBarPresentationData?, enableMediaAccessoryPanel: Bool, locationBroadcastPanelSource: LocationBroadcastPanelSource) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.enableMediaAccessoryPanel = enableMediaAccessoryPanel self.locationBroadcastPanelSource = locationBroadcastPanelSource @@ -153,11 +157,27 @@ public class TelegramController: ViewController { }) } } + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.mediaAccessoryPanel?.0.containerNode.updatePresentationData(presentationData) + strongSelf.locationBroadcastAccessoryPanel?.updatePresentationData(presentationData) + } + } + }) } deinit { self.mediaStatusDisposable?.dispose() self.locationBroadcastDisposable?.dispose() + self.presentationDataDisposable?.dispose() } required public init(coder aDecoder: NSCoder) { @@ -183,7 +203,7 @@ public class TelegramController: ViewController { locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) } else { let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } - locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(theme: presentationData.theme, strings: presentationData.strings, tapAction: { [weak self] in + locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(accountPeerId: self.account.peerId, theme: presentationData.theme, strings: presentationData.strings, tapAction: { [weak self] in if let strongSelf = self { switch strongSelf.locationBroadcastPanelSource { case .none: @@ -202,7 +222,7 @@ public class TelegramController: ViewController { } var items: [ActionSheetItem] = [] if !messages.isEmpty { - items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(locationBroadcastPeers.count)))) + items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(messages.count)))) for message in messages { if let peer = message.peers[message.id.peerId] { var beginTimeAndTimeout: (Double, Double)? @@ -213,7 +233,7 @@ public class TelegramController: ViewController { } if let beginTimeAndTimeout = beginTimeAndTimeout { - items.append(LocationBroadcastActionSheetItem(title: peer.displayTitle, beginTimestamp: beginTimeAndTimeout.0, timeout: beginTimeAndTimeout.1, strings: presentationData.strings, action: { + items.append(LocationBroadcastActionSheetItem(account: strongSelf.account, peer: peer, title: peer.displayTitle, beginTimestamp: beginTimeAndTimeout.0, timeout: beginTimeAndTimeout.1, strings: presentationData.strings, action: { dismissAction() if let strongSelf = self { presentLiveLocationController(account: strongSelf.account, peerId: peer.id, controller: strongSelf) diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index b0f571cc54..719a422f95 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -34,6 +34,12 @@ func legacyAccountGet() -> Account? { } private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponentsAccessChecker { + private weak var applicationContext: TelegramApplicationContext? + + init(applicationContext: TelegramApplicationContext?) { + self.applicationContext = applicationContext + } + public func checkAddressBookAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { return true } @@ -51,6 +57,24 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } public func checkLocationAuthorizationStatus(for intent: TGLocationAccessIntent, alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { + let subject: DeviceAccessLocationSubject + if intent == TGLocationAccessIntentSend { + subject = .send + } else if intent == TGLocationAccessIntentLiveLocation { + subject = .live + } else if intent == TGLocationAccessIntentTracking { + subject = .tracking + } else { + assertionFailure() + subject = .send + } + if let applicationContext = self.applicationContext { + authorizeDeviceAccess(to: .location(subject), presentationData: applicationContext.currentPresentationData.with { $0 }, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in + if !value { + alertDismissCompletion?() + } + }) + } return true } } @@ -163,7 +187,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func accessChecker() -> LegacyComponentsAccessChecker! { - return LegacyComponentsAccessCheckerImpl() + return LegacyComponentsAccessCheckerImpl(applicationContext: legacyAccount?.telegramApplicationContext) } public func stickerPacksSignal() -> SSignal! { @@ -393,27 +417,4 @@ public func initializeLegacyComponents(application: UIApplication, currentSizeCl ASActor.registerClass(LegacyImageDownloadActor.self) LegacyComponentsGlobals.setProvider(LegacyComponentsGlobalsProviderImpl()) - //freedomUIKitInit(); - - /*TGHacks.setApplication(application) - TGLegacyComponentsSetAccessChecker(AccessCheckerImpl()) - TGHacks.setPauseMusicPlayer { - - } - TGViewController.setSizeClassSignal { - return SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber) - } - TGLegacyComponentsSetDocumentsPath(documentsPath) - - TGLegacyComponentsSetCanOpenURL({ url in - if let url = url { - return canOpenUrl(url) - } - return false - }) - TGLegacyComponentsSetOpenURL({ url in - if let url = url { - return openUrl(url) - } - })*/ } diff --git a/TelegramUI/TelegramRootController.swift b/TelegramUI/TelegramRootController.swift index e1bb0e293c..6fcfb7b4c2 100644 --- a/TelegramUI/TelegramRootController.swift +++ b/TelegramUI/TelegramRootController.swift @@ -107,9 +107,9 @@ public final class TelegramRootController: NavigationController { } public func openRootCamera() { - guard let chatListController = self.chatListController else { + guard let controller = self.viewControllers.last as? ViewController else { return } - presentedLegacyShortcutCamera(account: self.account, saveCapturedMedia: false, saveEditedPhotos: false, parentController: chatListController) + presentedLegacyShortcutCamera(account: self.account, saveCapturedMedia: false, saveEditedPhotos: false, parentController: controller) } } diff --git a/TelegramUI/TelegramVideoNode.swift b/TelegramUI/TelegramVideoNode.swift deleted file mode 100644 index 067441a22c..0000000000 --- a/TelegramUI/TelegramVideoNode.swift +++ /dev/null @@ -1,521 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore - -import LegacyComponents - -private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdge, view: TGEmbedPIPPullArrowView) { - let arrowX: CGFloat - switch edge { - case .left: - view.transform = .identity - arrowX = size.width - 40.0 + floor((40.0 - view.bounds.size.width) / 2.0) - case .right: - view.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) - arrowX = floor((40.0 - view.bounds.size.width) / 2.0) - } - - view.frame = CGRect(origin: CGPoint(x: arrowX, y: floor((size.height - view.bounds.size.height) / 2.0)), size: view.bounds.size) -} - -private final class SharedTelegramVideoContext: SharedVideoContext { - let player: MediaPlayer - let playerNode: MediaPlayerNode - - private let playbackCompletedListeners = Bag<() -> Void>() - - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource) { - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) - var actionAtEndImpl: (() -> Void)? - self.player.actionAtEnd = .stop - self.playerNode = MediaPlayerNode(backgroundThread: false) - self.player.attachPlayerNode(self.playerNode) - - super.init() - - actionAtEndImpl = { [weak self] in - if let strongSelf = self { - for listener in strongSelf.playbackCompletedListeners.copyItems() { - listener() - } - } - } - } - - func play() { - assert(Queue.mainQueue().isCurrent()) - self.player.play() - } - - func pause() { - assert(Queue.mainQueue().isCurrent()) - self.player.pause() - } - - func togglePlayPause() { - assert(Queue.mainQueue().isCurrent()) - self.player.togglePlayPause() - } - - func setSoundEnabled(_ value: Bool) { - assert(Queue.mainQueue().isCurrent()) - if value { - self.player.playOnceWithSound(playAndRecord: false) - } else { - self.player.continuePlayingWithoutSound() - } - } - - func seek(_ timestamp: Double) { - assert(Queue.mainQueue().isCurrent()) - self.player.seek(timestamp: timestamp) - } - - func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { - return self.playbackCompletedListeners.add(f) - } - - func removePlaybackCompleted(_ index: Int) { - self.playbackCompletedListeners.remove(index) - } -} - -enum TelegramVideoNodeSource { - case messageMedia(stableId: UInt32, file: TelegramMediaFile) - - fileprivate var id: TelegramVideoNodeMessageMediaId { - switch self { - case let .messageMedia(stableId, _): - return TelegramVideoNodeMessageMediaId(stableId: stableId) - } - } - - fileprivate var resource: MediaResource { - switch self { - case let .messageMedia(_, file): - return file.resource - } - } - - fileprivate var file: TelegramMediaFile { - switch self { - case let .messageMedia(_, file): - return file - } - } -} - -private struct TelegramVideoNodeMessageMediaId: Hashable { - let stableId: UInt32 - - static func ==(lhs: TelegramVideoNodeMessageMediaId, rhs: TelegramVideoNodeMessageMediaId) -> Bool { - return lhs.stableId == rhs.stableId - } - - var hashValue: Int { - return self.stableId.hashValue - } -} - -private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayPlainVideoShadow")?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(top: 22.0, left: 25.0, bottom: 26.0, right: 25.0), resizingMode: .stretch) - -final class TelegramVideoNode: OverlayMediaItemNode { - private let manager: MediaManager - private let source: TelegramVideoNodeSource - private let priority: Int32 - private let withSound: Bool - private let postbox: Postbox - - private var soundEnabled: Bool - - private var contextId: Int32? - - private var context: SharedTelegramVideoContext? - private var contextPlaybackEndedIndex: Int? - private var validLayout: CGSize? - - private let backgroundNode: ASImageNode - private let imageNode: TransformImageNode - private var snapshotView: UIView? - private let progressNode: RadialProgressNode - private let controlsNode: PictureInPictureVideoControlsNode? - private var minimizedBlurView: UIVisualEffectView? - private var minimizedArrowView: TGEmbedPIPPullArrowView? - private var minimizedEdge: OverlayMediaItemMinimizationEdge? - - private var statusDisposable: Disposable? - - var playbackEnded: (() -> Void)? - var tapped: (() -> Void)? - var dismissed: (() -> Void)? - var unembed: (() -> Void)? - - private var initializedStatus = false - private let _status = Promise() - var status: Signal { - return self._status.get() - } - - override var group: OverlayMediaItemNodeGroup? { - return OverlayMediaItemNodeGroup(rawValue: 0) - } - - let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - override var isMinimizeable: Bool { - return true - } - - init(manager: MediaManager, account: Account, source: TelegramVideoNodeSource, priority: Int32, withSound: Bool, withOverlayControls: Bool = false) { - self.manager = manager - self.source = source - self.priority = priority - self.withSound = withSound - self.soundEnabled = withSound - self.postbox = account.postbox - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.imageNode = TransformImageNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor(white: 1.0, alpha: 1.0), icon: nil)) - - var leaveImpl: (() -> Void)? - var togglePlayPauseImpl: (() -> Void)? - var closeImpl: (() -> Void)? - - if withOverlayControls { - let controlsNode = PictureInPictureVideoControlsNode(leave: { - leaveImpl?() - }, playPause: { - togglePlayPauseImpl?() - }, close: { - closeImpl?() - }) - controlsNode.alpha = 0.0 - self.controlsNode = controlsNode - } else { - self.controlsNode = nil - } - - super.init() - - leaveImpl = { [weak self] in - self?.unembed?() - } - - togglePlayPauseImpl = { [weak self] in - self?.togglePlayPause() - } - - closeImpl = { [weak self] in - if let strongSelf = self { - if withOverlayControls { - strongSelf.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in - self?.dismiss() - }) - strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - } else { - strongSelf.dismiss() - } - } - } - - if withOverlayControls { - self.backgroundNode.image = backgroundImage - } - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.imageNode) - if let controlsNode = self.controlsNode { - self.addSubnode(controlsNode) - } - - self.imageNode.setSignal(chatMessageVideo(postbox: account.postbox, video: source.file)) - } - - deinit { - if let context = self.context { - if context.playerNode.supernode === self { - context.playerNode.removeFromSupernode() - } - } - - let manager = self.manager - let source = self.source - let contextId = self.contextId - - Queue.mainQueue().async { - if let contextId = contextId { - manager.sharedVideoContextManager.detachSharedVideoContext(id: source.id, index: contextId) - } - } - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - private func updateContext(_ context: SharedTelegramVideoContext?) { - assert(Queue.mainQueue().isCurrent()) - - let previous = self.context - self.context = context - if previous !== context { - if let snapshotView = self.snapshotView { - snapshotView.removeFromSuperview() - self.snapshotView = nil - } - if let previous = previous { - if let contextPlaybackEndedIndex = self.contextPlaybackEndedIndex { - previous.removePlaybackCompleted(contextPlaybackEndedIndex) - } - self.contextPlaybackEndedIndex = nil - /*if let snapshotView = previous.playerNode.view.snapshotView(afterScreenUpdates: false) { - self.snapshotView = snapshotView - snapshotView.frame = self.imageNode.frame - self.view.addSubview(snapshotView) - }*/ - if previous.playerNode.supernode === self { - previous.playerNode.removeFromSupernode() - } - } - if let context = context { - self.contextPlaybackEndedIndex = context.addPlaybackCompleted { [weak self] in - self?.playbackEnded?() - } - if context.playerNode.supernode !== self { - if let controlsNode = self.controlsNode { - self.insertSubnode(context.playerNode, belowSubnode: controlsNode) - } else { - self.addSubnode(context.playerNode) - } - if let validLayout = self.validLayout { - self.updateLayoutImpl(validLayout) - } - } - } - if self.hasAttachedContext != (context !== nil) { - self.hasAttachedContext = (context !== nil) - self.hasAttachedContextUpdated?(self.hasAttachedContext) - } - } - self.imageNode.isHidden = !self.hasAttachedContext - } - - override func layout() { - self.updateLayout(self.bounds.size) - } - - override func updateLayout(_ size: CGSize) { - if size != self.validLayout { - self.updateLayoutImpl(size) - } - } - - private func updateLayoutImpl(_ size: CGSize) { - self.validLayout = size - - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()) - let videoFrame = CGRect(origin: CGPoint(), size: arguments.boundingSize) - - if let context = self.context { - context.playerNode.transformArguments = arguments - context.playerNode.frame = videoFrame - } - - let backgroundInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) - self.backgroundNode.frame = CGRect(origin: CGPoint(x: -backgroundInsets.left, y: -backgroundInsets.top), size: CGSize(width: videoFrame.size.width + backgroundInsets.left + backgroundInsets.right, height: videoFrame.size.height + backgroundInsets.top + backgroundInsets.bottom)) - - self.imageNode.asyncLayout()(arguments)() - self.imageNode.frame = videoFrame - self.snapshotView?.frame = self.imageNode.frame - - if let controlsNode = self.controlsNode { - controlsNode.frame = videoFrame - controlsNode.updateLayout(size: videoFrame.size, transition: .immediate) - } - - if let minimizedBlurView = self.minimizedBlurView { - minimizedBlurView.frame = videoFrame - } - - if let minimizedArrowView = self.minimizedArrowView, let minimizedEdge = self.minimizedEdge { - setupArrowFrame(size: videoFrame.size, edge: minimizedEdge, view: minimizedArrowView) - } - } - - func play() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - context.play() - } - }) - } - - func pause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - context.pause() - } - }) - } - - func togglePlayPause() { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - context.togglePlayPause() - } - }) - } - - func setSoundEnabled(_ value: Bool) { - self.soundEnabled = value - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - context.setSoundEnabled(value) - } - }) - } - - func seek(_ timestamp: Double) { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - context.seek(timestamp) - } - }) - } - - override func setShouldAcquireContext(_ value: Bool) { - if value { - if self.contextId == nil { - self.contextId = self.manager.sharedVideoContextManager.attachSharedVideoContext(id: source.id, priority: self.priority, create: { - let context = SharedTelegramVideoContext(audioSessionManager: manager.audioSession, postbox: self.postbox, resource: self.source.resource) - context.setSoundEnabled(self.soundEnabled) - //context.play() - return context - }, update: { [weak self] context in - if let strongSelf = self { - strongSelf.updateContext(context as? SharedTelegramVideoContext) - } - }) - } - } else if let contextId = self.contextId { - self.manager.sharedVideoContextManager.detachSharedVideoContext(id: self.source.id, index: contextId) - self.contextId = nil - } - - if !self.initializedStatus { - self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { context in - if let context = context as? SharedTelegramVideoContext { - self.initializedStatus = true - self._status.set(context.player.status) - self.controlsNode?.status = context.player.status - } - }) - } - } - - override func preferredSizeForOverlayDisplay() -> CGSize { - switch self.source { - case let .messageMedia(_, file): - if let dimensions = file.dimensions { - return dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0)) - } - } - return CGSize(width: 100.0, height: 100.0) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?() - - if let controlsNode = self.controlsNode { - if controlsNode.alpha.isZero { - controlsNode.alpha = 1.0 - controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } else { - controlsNode.alpha = 0.0 - controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - - if let _ = self.minimizedEdge { - self.unminimize?() - } - } - } - - override func dismiss() { - self.dismissed?() - } - - override func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { - if self.minimizedEdge == edge { - if let minimizedArrowView = self.minimizedArrowView { - minimizedArrowView.setAngled(!adjusting, animated: true) - } - return - } - - self.minimizedEdge = edge - - if let edge = edge { - if self.minimizedBlurView == nil { - let minimizedBlurView = UIVisualEffectView(effect: nil) - self.minimizedBlurView = minimizedBlurView - minimizedBlurView.frame = self.bounds - minimizedBlurView.isHidden = true - self.view.addSubview(minimizedBlurView) - } - if self.minimizedArrowView == nil { - let minimizedArrowView = TGEmbedPIPPullArrowView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 8.0, height: 38.0))) - minimizedArrowView.alpha = 0.0 - self.minimizedArrowView = minimizedArrowView - self.minimizedBlurView?.contentView.addSubview(minimizedArrowView) - } - if let minimizedArrowView = self.minimizedArrowView { - setupArrowFrame(size: self.bounds.size, edge: edge, view: minimizedArrowView) - minimizedArrowView.setAngled(!adjusting, animated: true) - } - } - - let effect: UIBlurEffect? = edge != nil ? UIBlurEffect(style: .light) : nil - if true { - if let edge = edge { - self.minimizedBlurView?.isHidden = false - - switch edge { - case .left: - break - case .right: - break - } - } - - UIView.animate(withDuration: 0.35, animations: { - self.minimizedBlurView?.effect = effect - self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0; - }, completion: { [weak self] finished in - if let strongSelf = self { - if finished && edge == nil { - strongSelf.minimizedBlurView?.isHidden = true - } - } - }) - } else { - self.minimizedBlurView?.effect = effect; - self.minimizedBlurView?.isHidden = edge == nil - self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0 - } - } -} diff --git a/TelegramUI/TermsOfServiceControllerNode.swift b/TelegramUI/TermsOfServiceControllerNode.swift index 395a1d4cda..0579b14261 100644 --- a/TelegramUI/TermsOfServiceControllerNode.swift +++ b/TelegramUI/TermsOfServiceControllerNode.swift @@ -128,8 +128,39 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { } } self.contentTextNode.tapAttributeAction = { [weak self] attributes in + guard let strongSelf = self else { + return + } if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { - self?.openUrl(url) + strongSelf.openUrl(url) + } else if let mention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention.mention), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention.mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, nil) + } else if let mention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, nil) } } self.contentTextNode.longTapAttributeAction = { [weak self] attributes in diff --git a/TelegramUI/ThemeGalleryItem.swift b/TelegramUI/ThemeGalleryItem.swift index 4e819d6698..951ed0b518 100644 --- a/TelegramUI/ThemeGalleryItem.swift +++ b/TelegramUI/ThemeGalleryItem.swift @@ -94,9 +94,14 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { if let largestSize = largestImageRepresentation(representations) { let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: representations), dispatchOnDisplayLink: false) + + let convertedRepresentations: [(TelegramMediaImageRepresentation, MediaResourceReference)] = representations.map({ ($0, .wallpaper(resource: $0.resource)) }) + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: account, representations: convertedRepresentations), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions, self.imageNode) - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + + if let largestIndex = convertedRepresentations.index(where: { $0.0 == largestSize }) { + self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: convertedRepresentations[largestIndex].1).start()) + } } else { self._ready.set(.single(Void())) } @@ -200,21 +205,9 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) - - /*if let (account, media) = self.accountAndEntry, let file = media as? TelegramMediaFile { - if isVisible { - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) - } else { - self.fetchDisposable.set(nil) - } - }*/ } override func title() -> Signal { return self._title.get() } - - /*override func footerContent() -> Signal { - return .single(self.footerContentNode) - }*/ } diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift index bc35860e51..62fcb4d66e 100644 --- a/TelegramUI/ThemeGridControllerNode.swift +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -149,17 +149,37 @@ final class ThemeGridControllerNode: ASDisplayNode { } }) - let transition = telegramWallpapers(account: account) - |> map { wallpapers -> (ThemeGridEntryTransition, Bool) in - var entries: [ThemeGridControllerEntry] = [] - var index = 0 - for item in wallpapers { - entries.append(ThemeGridControllerEntry(index: index, wallpaper: item)) - index += 1 - } - let previous = previousEntries.swap(entries) - return (preparedThemeGridEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), previous == nil) + let transition = combineLatest(telegramWallpapers(postbox: account.postbox, network: account.network), account.telegramApplicationContext.presentationData) + |> map { wallpapers, presentationData -> (ThemeGridEntryTransition, Bool) in + var entries: [ThemeGridControllerEntry] = [] + var index = 0 + + switch presentationData.theme.name { + case let .builtin(name): + switch name { + case .dayClassic: + break + case .day: + entries.append(ThemeGridControllerEntry(index: index, wallpaper: .color(0xffffff))) + index += 1 + case .nightGrayscale: + entries.append(ThemeGridControllerEntry(index: index, wallpaper: .color(0x000000))) + index += 1 + case .nightAccent: + entries.append(ThemeGridControllerEntry(index: index, wallpaper: .color(0x18222D))) + index += 1 + } + default: + break } + + for item in wallpapers { + entries.append(ThemeGridControllerEntry(index: index, wallpaper: item)) + index += 1 + } + let previous = previousEntries.swap(entries) + return (preparedThemeGridEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), previous == nil) + } self.disposable = (transition |> deliverOnMainQueue).start(next: { [weak self] (transition, _) in if let strongSelf = self { strongSelf.enqueueTransition(transition) diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift index cc69ae8db6..f4846dd271 100644 --- a/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -4,11 +4,11 @@ import Postbox import SwiftSignalKit import Display -public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, media: Media, opportunistic: Bool) -> Signal { - switch media { +public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, media: AnyMediaReference, opportunistic: Bool) -> Signal { + switch media.media { case let file as TelegramMediaFile: let signal = Signal { subscriber in - let fetch = postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes))).start() + let fetch = postbox.mediaBox.fetchedResource(file.resource, parameters: nil).start() let data = postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) if next.complete { @@ -30,54 +30,75 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } return result - |> mapToSignal { data -> Signal in - if data.complete { - if file.mimeType.hasPrefix("image/") { - return Signal { subscriber in - if let image = UIImage(contentsOfFile: data.path), let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), contextGenerator: { size, context in - context.setBlendMode(.copy) - context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) - }, scale: 1.0), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) { - let imageDimensions = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + |> mapToSignal { data -> Signal in + if data.complete { + if file.mimeType.hasPrefix("image/") { + return Signal { subscriber in + if let fullSizeData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + let imageOrientation = imageOrientationFromSource(imageSource) - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) - postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) - - let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) - - var attributes = file.attributes - loop: for i in 0 ..< attributes.count { - switch attributes[i] { - case .ImageSize: - attributes.remove(at: i) - break loop - default: - break + let image = UIImage(cgImage: cgImage, scale: 1.0, orientation: imageOrientation) + + if let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), contextGenerator: { size, context in + context.setBlendMode(.copy) + drawImage(context: context, image: image.cgImage!, orientation: image.imageOrientation, in: CGRect(origin: CGPoint(), size: size)) + }, scale: 1.0), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) { + let imageDimensions = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + + let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) + + let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) + + var attributes = file.attributes + loop: for i in 0 ..< attributes.count { + switch attributes[i] { + case .ImageSize: + attributes.remove(at: i) + break loop + default: + break + } } + attributes.append(.ImageSize(size: imageDimensions)) + let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: scaledImageSize, resource: thumbnailResource)]).withUpdatedAttributes(attributes) + subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putCompletion() + } else { + let updatedFile = file.withUpdatedSize(data.size) + subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putCompletion() } - attributes.append(.ImageSize(size: imageDimensions)) - subscriber.putNext(file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: scaledImageSize, resource: thumbnailResource)]).withUpdatedAttributes(attributes)) - subscriber.putCompletion() } else { - subscriber.putNext(file.withUpdatedSize(data.size)) + let updatedFile = file.withUpdatedSize(data.size) + subscriber.putNext(.standalone(media: updatedFile)) subscriber.putCompletion() } - - return EmptyDisposable - } |> runOn(opportunistic ? Queue.mainQueue() : Queue.concurrentDefaultQueue()) - } else { - return .single(file.withUpdatedSize(data.size)) - } - } else if opportunistic { - return .single(nil) + } else { + let updatedFile = file.withUpdatedSize(data.size) + subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putCompletion() + } + + return EmptyDisposable + } |> runOn(opportunistic ? Queue.mainQueue() : Queue.concurrentDefaultQueue()) } else { - return .complete() + let updatedFile = file.withUpdatedSize(data.size) + return .single(.standalone(media: updatedFile)) } + } else if opportunistic { + return .single(nil) + } else { + return .complete() } + } case let image as TelegramMediaImage: if let representation = largestImageRepresentation(image.representations) { let signal = Signal { subscriber in - let fetch = postbox.mediaBox.fetchedResource(representation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start() + let fetch = postbox.mediaBox.fetchedResource(representation.resource, parameters: nil).start() let data = postbox.mediaBox.resourceData(representation.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) if next.complete { @@ -99,15 +120,15 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } return result - |> mapToSignal { data -> Signal in - if data.complete { - return .single(nil) - } else if opportunistic { - return .single(nil) - } else { - return .complete() - } + |> mapToSignal { data -> Signal in + if data.complete { + return .single(nil) + } else if opportunistic { + return .single(nil) + } else { + return .complete() } + } } else { return .single(nil) } diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index fd4189b21d..1dedf40688 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -8,6 +8,7 @@ import Postbox enum UniversalVideoGalleryItemContentInfo { case message(Message) + case webPage(TelegramMediaWebpage, TelegramMediaFile) } class UniversalVideoGalleryItem: GalleryItem { @@ -49,22 +50,28 @@ class UniversalVideoGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - guard let contentInfo = self.contentInfo, case let .message(message) = contentInfo else { + guard let contentInfo = self.contentInfo else { return nil } - if let id = message.groupInfo?.stableId { - var media: Media? - for m in message.media { - if let m = m as? TelegramMediaImage { - media = m - } else if let m = m as? TelegramMediaFile, m.isVideo { - media = m + if case let .message(message) = contentInfo { + if let id = message.groupInfo?.stableId { + var mediaReference: AnyMediaReference? + for m in message.media { + if let m = m as? TelegramMediaImage { + mediaReference = .message(message: MessageReference(message), media: m) + } else if let m = m as? TelegramMediaFile, m.isVideo { + mediaReference = .message(message: MessageReference(message), media: m) + } + } + if let mediaReference = mediaReference { + if let item = ChatMediaGalleryThumbnailItem(account: self.account, mediaReference: mediaReference) { + return (Int64(id), item) + } } } - if let media = media { - if let item = ChatMediaGalleryThumbnailItem(account: self.account, media: media) { - return (Int64(id), item) - } + } else if case let .webPage(webPage, file) = contentInfo { + if let item = ChatMediaGalleryThumbnailItem(account: self.account, mediaReference: .webPage(webPage: WebpageReference(webPage), media: file)) { + return (0, item) } } return nil @@ -294,7 +301,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isAnimated = false var isInstagram = false if let content = item.content as? NativeVideoContent { - isAnimated = content.file.isAnimated + isAnimated = content.fileReference.media.isAnimated } else if let _ = item.content as? SystemVideoContent { isInstagram = true self._title.set(.single(item.strings.Message_Video)) @@ -324,6 +331,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message): self.footerContentNode.setMessage(message) + case .webPage: + break } } self.footerContentNode.setup(origin: item.originData, caption: item.caption) @@ -637,37 +646,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return self._rightBarButtonItem.get() } - /*private func activateVideo() { - if let (account, file, _) = self.accountAndFile { - if let resourceStatus = self.resourceStatus { - switch resourceStatus { - case .Fetching: - break - case .Local: - self.playVideo() - case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) - } - } - } - }*/ - @objc func statusButtonPressed() { if let videoNode = self.videoNode { videoNode.togglePlayPause() } - /*if let (account, file, _) = self.accountAndFile { - if let resourceStatus = self.resourceStatus { - switch resourceStatus { - case .Fetching: - account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) - case .Local: - self.playVideo() - case .Remote: - self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) - } - } - }*/ } @objc func pictureInPictureButtonPressed() { @@ -708,6 +690,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return nil })) + case .webPage: + break } } account.telegramApplicationContext.mediaManager.setOverlayVideoNode(overlayNode) @@ -718,41 +702,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } } - /*if let account = self.accountAndFile?.0, let message = self.message, let file = self.accountAndFile?.1 { - let overlayNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: TelegramVideoNodeSource.messageMedia(stableId: message.stableId, file: file), priority: 1, withSound: true, withOverlayControls: true) - overlayNode.dismissed = { [weak account, weak overlayNode] in - if let account = account, let overlayNode = overlayNode { - if overlayNode.supernode != nil { - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) - } - } - } - let baseNavigationController = self.baseNavigationController() - overlayNode.unembed = { [weak account, weak overlayNode, weak baseNavigationController] in - if let account = account { - let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in - if let baseNavigationController = baseNavigationController { - baseNavigationController.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: baseNavigationController) - - (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { _, _ in - if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: overlayNode, transitionContainerNode: overlaySupernode, transitionBackgroundNode: ASDisplayNode()) - } - return nil - })) - } - } - overlayNode.setShouldAcquireContext(true) - account.telegramApplicationContext.mediaManager.setOverlayVideoNode(overlayNode) - if overlayNode.supernode != nil { - self.beginCustomDismiss() - self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() - }) - } - }*/ } override func footerContent() -> Signal { diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 7aeb955660..d31383f518 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -383,12 +383,20 @@ private struct UserInfoState: Equatable { } } -private func stringForBlockAction(strings: PresentationStrings, action: DestructiveUserInfoAction) -> String { +private func stringForBlockAction(strings: PresentationStrings, action: DestructiveUserInfoAction, peer: Peer) -> String { switch action { case .block: - return strings.Conversation_BlockUser + if let user = peer as? TelegramUser, user.botInfo != nil { + return strings.Bot_Stop + } else { + return strings.Conversation_BlockUser + } case .unblock: - return strings.Conversation_UnblockUser + if let user = peer as? TelegramUser, user.botInfo != nil { + return strings.Bot_Unblock + } else { + return strings.Conversation_UnblockUser + } case .removeContact: return strings.UserInfo_DeleteContact } @@ -503,7 +511,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat entries.append(UserInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: messageSound, default: globalNotificationSettings.effective.privateChats.sound))) if view.peerIsContact { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact), .removeContact)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact, peer: user), .removeContact)) } } else { if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint { @@ -512,9 +520,9 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat if let cachedData = view.cachedData as? CachedUserData { if cachedData.isBlocked { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock), .unblock)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock, peer: user), .unblock)) } else { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .block), .block)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .block, peer: user), .block)) } } } @@ -716,9 +724,15 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll pushControllerImpl?(groupsInCommonController(account: account, peerId: peerId)) }, updatePeerBlocked: { value in let _ = (account.postbox.loadedPeerWithId(peerId) - |> take(1) - |> deliverOnMainQueue).start(next: { peer in - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + if let user = peer as? TelegramUser, user.botInfo != nil { + updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) + if !value { + let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + } + } else { let text: String if value { text = presentationData.strings.UserInfo_BlockConfirmation(peer.displayTitle).0 @@ -728,7 +742,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) })]), nil) - }) + } + }) }, deleteContact: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift index 3733d6d4d8..a6b41e78d8 100644 --- a/TelegramUI/UsernameSetupController.swift +++ b/TelegramUI/UsernameSetupController.swift @@ -303,6 +303,7 @@ public func usernameSetupController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) controller.enableInteractiveDismiss = true dismissImpl = { [weak controller] in + controller?.view.endEditing(true) controller?.dismiss() } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index 10d7b35935..af061389e2 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -221,7 +221,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 55.0, height: 55.0), resource: imageResource) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: tmpImage) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photoReference: .standalone(media: tmpImage)) } else { updateIconImageSignal = .complete() } diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift index 62f480c5fd..879fe0d835 100644 --- a/TelegramUI/WebEmbedVideoContent.swift +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -35,22 +35,24 @@ func webEmbedVideoContentSupportsWebpage(_ webpageContent: TelegramMediaWebpageL final class WebEmbedVideoContent: UniversalVideoContent { let id: AnyHashable + let webPage: TelegramMediaWebpage let webpageContent: TelegramMediaWebpageLoadedContent let dimensions: CGSize let duration: Int32 - init?(webpageContent: TelegramMediaWebpageLoadedContent) { + init?(webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent) { guard let embedUrl = webpageContent.embedUrl else { return nil } self.id = AnyHashable(embedUrl) + self.webPage = webPage self.webpageContent = webpageContent self.dimensions = webpageContent.embedSize ?? CGSize(width: 128.0, height: 128.0) self.duration = Int32(webpageContent.duration ?? (0 as Int)) } func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webpageContent: self.webpageContent) + return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webPage: self.webPage, webpageContent: self.webpageContent) } } @@ -95,7 +97,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte private var loadProgressDisposable: Disposable? private var statusDisposable: Disposable? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webpageContent: TelegramMediaWebpageLoadedContent) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent) { self.webpageContent = webpageContent let converted = TGWebPageMediaAttachment() @@ -165,7 +167,8 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte }) if let image = webpageContent.image { - self.thumbnailDisposable = (rawMessagePhoto(postbox: postbox, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in + self.thumbnailDisposable = (rawMessagePhoto(postbox: postbox, photoReference: .webPage(webPage: WebpageReference(webPage), media: image)) + |> deliverOnMainQueue).start(next: { [weak self] image in if let strongSelf = self { strongSelf.thumbnail.set(.single(image)) strongSelf._ready.set(.single(Void())) diff --git a/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift index 4a81c5f2f6..2621879ac2 100644 --- a/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift +++ b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift @@ -23,7 +23,7 @@ private final class LegacyLocationVenueIconTask: NSObject { } } })) - self.disposable.add(account.postbox.mediaBox.fetchedResource(resource, tag: nil).start()) + self.disposable.add(account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start()) } deinit {