diff --git a/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@2x.png b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@2x.png new file mode 100644 index 0000000000..066e8125a1 Binary files /dev/null and b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@2x.png differ diff --git a/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@3x.png b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@3x.png new file mode 100644 index 0000000000..3d21a4e764 Binary files /dev/null and b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Apple_Pay_Payment_Mark@3x.png differ diff --git a/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Contents.json b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Contents.json new file mode 100644 index 0000000000..c6222ea232 --- /dev/null +++ b/Images.xcassets/Bot Payments/ApplePayLogo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Apple_Pay_Payment_Mark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Apple_Pay_Payment_Mark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Bot Payments/Contents.json b/Images.xcassets/Bot Payments/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Bot Payments/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/Contents.json b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/Contents.json new file mode 100644 index 0000000000..df2fbf0d29 --- /dev/null +++ b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "VideoMessagePIPShadow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "VideoMessagePIPShadow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@2x.png b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@2x.png new file mode 100644 index 0000000000..d36a000e93 Binary files /dev/null and b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@2x.png differ diff --git a/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@3x.png b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@3x.png new file mode 100644 index 0000000000..53a2a0e293 Binary files /dev/null and b/Images.xcassets/Chat/Message/OverlayInstantVideoShadow.imageset/VideoMessagePIPShadow@3x.png differ diff --git a/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/Contents.json b/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/Contents.json new file mode 100644 index 0000000000..7b37fd4a53 --- /dev/null +++ b/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPictureShadow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/PictureInPictureShadow@2x.png b/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/PictureInPictureShadow@2x.png new file mode 100644 index 0000000000..695a2278fc Binary files /dev/null and b/Images.xcassets/Chat/Message/OverlayPlainVideoShadow.imageset/PictureInPictureShadow@2x.png differ diff --git a/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/Contents.json b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/Contents.json new file mode 100644 index 0000000000..3b767d8bbe --- /dev/null +++ b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "GalleryEmbeddedStickersIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "GalleryEmbeddedStickersIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@2x.png b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@2x.png new file mode 100644 index 0000000000..dce30d8177 Binary files /dev/null and b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@2x.png differ diff --git a/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@3x.png b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@3x.png new file mode 100644 index 0000000000..6fe4d5e5c3 Binary files /dev/null and b/Images.xcassets/Media Gallery/AssociatedMasksButton.imageset/GalleryEmbeddedStickersIcon@3x.png differ diff --git a/Images.xcassets/Media Gallery/Contents.json b/Images.xcassets/Media Gallery/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Media Gallery/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/Contents.json new file mode 100644 index 0000000000..a3f6e6383b --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "EmbedVideoPIPIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/EmbedVideoPIPIcon@2x.png b/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/EmbedVideoPIPIcon@2x.png new file mode 100644 index 0000000000..9c6de4b160 Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPictureButton.imageset/EmbedVideoPIPIcon@2x.png differ diff --git a/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/Contents.json new file mode 100644 index 0000000000..1fa52c21ba --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPictureClose@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/PictureInPictureClose@2x.png b/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/PictureInPictureClose@2x.png new file mode 100644 index 0000000000..318f151bfa Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPictureClose.imageset/PictureInPictureClose@2x.png differ diff --git a/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/Contents.json new file mode 100644 index 0000000000..1159a66d69 --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPictureIndicator@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/PictureInPictureIndicator@2x.png b/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/PictureInPictureIndicator@2x.png new file mode 100644 index 0000000000..393a36363d Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPictureIcon.imageset/PictureInPictureIndicator@2x.png differ diff --git a/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/Contents.json new file mode 100644 index 0000000000..39aad4c096 --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPictureExit@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/PictureInPictureExit@2x.png b/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/PictureInPictureExit@2x.png new file mode 100644 index 0000000000..b70125cc8a Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPictureLeave.imageset/PictureInPictureExit@2x.png differ diff --git a/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/Contents.json new file mode 100644 index 0000000000..60b161bd0a --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPicturePause@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/PictureInPicturePause@2x.png b/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/PictureInPicturePause@2x.png new file mode 100644 index 0000000000..dfb719be19 Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPicturePause.imageset/PictureInPicturePause@2x.png differ diff --git a/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/Contents.json b/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/Contents.json new file mode 100644 index 0000000000..a587301918 --- /dev/null +++ b/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PictureInPicturePlay@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/PictureInPicturePlay@2x.png b/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/PictureInPicturePlay@2x.png new file mode 100644 index 0000000000..0b229cd44c Binary files /dev/null and b/Images.xcassets/Media Gallery/PictureInPicturePlay.imageset/PictureInPicturePlay@2x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 59b8d8ee7b..0909bc0694 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ D00ADFD91EBA2E9D00873D2E /* OngoingCallThreadLocalContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0EC6FFA1EBA1DE900EBF1C3 /* OngoingCallThreadLocalContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D00ADFDB1EBA2EAF00873D2E /* OngoingCallContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDA1EBA2EAF00873D2E /* OngoingCallContext.swift */; }; D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */; }; - D00ADFDF1EBB944900873D2E /* VideoOverlayMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDE1EBB944900873D2E /* VideoOverlayMediaItem.swift */; }; D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00BDA1E1EE5B69200C64C5E /* ChannelAdminController.swift */; }; D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA171ECC8E0000295217 /* CallListController.swift */; }; D01BAA1A1ECC8E0D00295217 /* CallListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA191ECC8E0D00295217 /* CallListControllerNode.swift */; }; @@ -20,7 +19,22 @@ D01BAA221ECE076100295217 /* CallListCallItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA211ECE076100295217 /* CallListCallItem.swift */; }; D01BAA241ECE173200295217 /* PresentationResourcesCallList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA231ECE173200295217 /* PresentationResourcesCallList.swift */; }; D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */; }; + D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */; }; + D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; D03E838F1EC10FE5001A6ED9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0FC40831D5B8E7400261D9D /* Info.plist */; }; + D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */; }; + D0471B4B1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B4A1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift */; }; + D0471B4F1EFD84600074D609 /* BotCheckoutPriceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B4E1EFD84600074D609 /* BotCheckoutPriceItem.swift */; }; + D0471B511EFD872F0074D609 /* CurrencyFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B501EFD872F0074D609 /* CurrencyFormat.swift */; }; + D0471B541EFD8ECA0074D609 /* currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = D0471B531EFD8ECA0074D609 /* currencies.json */; }; + D0471B561EFDB40F0074D609 /* BotCheckoutActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B551EFDB40F0074D609 /* BotCheckoutActionButton.swift */; }; + D0471B581EFE6D020074D609 /* BotCheckoutInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B571EFE6D020074D609 /* BotCheckoutInfoController.swift */; }; + D0471B5A1EFE70400074D609 /* BotCheckoutInfoControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B591EFE70400074D609 /* BotCheckoutInfoControllerNode.swift */; }; + D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B5B1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift */; }; + D0471B5E1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B5D1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift */; }; + D0471B601EFEB5A70074D609 /* BotPaymentTextItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B5F1EFEB5A70074D609 /* BotPaymentTextItemNode.swift */; }; + D0471B621EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B611EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift */; }; + D0471B641EFEB5CB0074D609 /* BotPaymentItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */; }; D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D101EEA04D400711AF6 /* MapResources.swift */; }; D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */; }; D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; }; @@ -29,9 +43,21 @@ D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */; }; D0754D241EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */; }; D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D261EEE10C800884F6E /* BotCheckoutController.swift */; }; + D079FCD91F05A5550038FADE /* BotCheckoutPasswordEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCD81F05A5550038FADE /* BotCheckoutPasswordEntryController.swift */; }; + D079FCDD1F05C4F20038FADE /* LocalAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCDC1F05C4F20038FADE /* LocalAuth.swift */; }; + D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCDE1F05C9280038FADE /* BotReceiptController.swift */; }; + D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCE01F05C9380038FADE /* BotReceiptControllerNode.swift */; }; + D079FCE91F06A76C0038FADE /* Notices.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCE81F06A76C0038FADE /* Notices.swift */; }; D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74C1EEFEE1500A3128C /* GameController.swift */; }; D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */; }; D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7501EEFF91E00A3128C /* GameControllerTitleView.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 */; }; + D09E63A41F0FAB91003444CD /* EmbedGalleryVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.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, ); }; }; D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB191EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift */; }; D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */; }; D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; }; @@ -45,6 +71,121 @@ D0C0B59F1EE082F5000F4D2C /* ChatSearchInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B59E1EE082F5000F4D2C /* ChatSearchInputPanelNode.swift */; }; D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */; }; D0C0B5B71EE1DEF1000F4D2C /* ThemeGridControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */; }; + D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; }; + D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; }; + D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */; }; + D0E9BA081F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA071F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift */; }; + D0E9BA0A1F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA091F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift */; }; + D0E9BA0C1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA0B1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift */; }; + D0E9BA141F05574500F079A4 /* STPCardValidationState.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA0D1F05574500F079A4 /* STPCardValidationState.h */; }; + D0E9BA151F05574500F079A4 /* STPCardValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA0E1F05574500F079A4 /* STPCardValidator.h */; }; + D0E9BA161F05574500F079A4 /* STPCardValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA0F1F05574500F079A4 /* STPCardValidator.m */; }; + D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA101F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h */; }; + D0E9BA181F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA111F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m */; }; + D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA121F05574500F079A4 /* STPPaymentCardTextField.h */; }; + D0E9BA1A1F05574500F079A4 /* STPPaymentCardTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA131F05574500F079A4 /* STPPaymentCardTextField.m */; }; + D0E9BA201F05577700F079A4 /* STPCardParams.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA1C1F05577700F079A4 /* STPCardParams.h */; }; + D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA1D1F05577700F079A4 /* STPCardParams.m */; }; + D0E9BA221F05577700F079A4 /* STPCard.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA1E1F05577700F079A4 /* STPCard.h */; }; + D0E9BA231F05577700F079A4 /* STPCard.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA1F1F05577700F079A4 /* STPCard.m */; }; + D0E9BA251F05578900F079A4 /* STPCardBrand.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA241F05578900F079A4 /* STPCardBrand.h */; }; + D0E9BA291F0557A600F079A4 /* STPFormEncodable.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA261F0557A600F079A4 /* STPFormEncodable.h */; }; + D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA271F0557A600F079A4 /* STPFormEncoder.h */; }; + D0E9BA2B1F0557A600F079A4 /* STPFormEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA281F0557A600F079A4 /* STPFormEncoder.m */; }; + D0E9BA2E1F0557D400F079A4 /* STPAddress.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA2C1F0557D400F079A4 /* STPAddress.h */; }; + D0E9BA2F1F0557D400F079A4 /* STPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA2D1F0557D400F079A4 /* STPAddress.m */; }; + D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA301F05583A00F079A4 /* STPPostalCodeValidator.h */; }; + D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA311F05583A00F079A4 /* STPPostalCodeValidator.m */; }; + D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA341F05585000F079A4 /* STPPhoneNumberValidator.h */; }; + D0E9BA371F05585000F079A4 /* STPPhoneNumberValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA351F05585000F079A4 /* STPPhoneNumberValidator.m */; }; + D0E9BA3A1F0558E800F079A4 /* NSString+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA381F0558E800F079A4 /* NSString+Stripe.h */; }; + D0E9BA3B1F0558E800F079A4 /* NSString+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA391F0558E800F079A4 /* NSString+Stripe.m */; }; + D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA3C1F0558FE00F079A4 /* STPSource.h */; }; + D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA3D1F0558FE00F079A4 /* StripeError.h */; }; + D0E9BA411F0558FE00F079A4 /* StripeError.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA3E1F0558FE00F079A4 /* StripeError.m */; }; + D0E9BA451F0559A500F079A4 /* STPAPIResponseDecodable.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA421F0559A500F079A4 /* STPAPIResponseDecodable.h */; }; + D0E9BA461F0559A500F079A4 /* NSDictionary+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA431F0559A500F079A4 /* NSDictionary+Stripe.h */; }; + D0E9BA471F0559A500F079A4 /* NSDictionary+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA441F0559A500F079A4 /* NSDictionary+Stripe.m */; }; + D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA481F0559B600F079A4 /* STPPaymentMethod.h */; }; + D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA4A1F0559C700F079A4 /* NSString+Stripe_CardBrands.h */; }; + D0E9BA4D1F0559C700F079A4 /* NSString+Stripe_CardBrands.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA4B1F0559C700F079A4 /* NSString+Stripe_CardBrands.m */; }; + D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA4E1F0559DA00F079A4 /* STPImageLibrary.h */; }; + D0E9BA521F0559DA00F079A4 /* STPImageLibrary.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA4F1F0559DA00F079A4 /* STPImageLibrary.m */; }; + D0E9BA531F0559DA00F079A4 /* STPImageLibrary+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA501F0559DA00F079A4 /* STPImageLibrary+Private.h */; }; + D0E9BA561F055A0B00F079A4 /* STPFormTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA541F055A0B00F079A4 /* STPFormTextField.h */; }; + D0E9BA571F055A0B00F079A4 /* STPFormTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA551F055A0B00F079A4 /* STPFormTextField.m */; }; + D0E9BA591F055A2200F079A4 /* STPWeakStrongMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA581F055A2200F079A4 /* STPWeakStrongMacros.h */; }; + D0E9BA5C1F055A3300F079A4 /* STPBINRange.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA5A1F055A3300F079A4 /* STPBINRange.h */; }; + D0E9BA5D1F055A3300F079A4 /* STPBINRange.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA5B1F055A3300F079A4 /* STPBINRange.m */; }; + D0E9BA601F055A4300F079A4 /* STPDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BA5E1F055A4300F079A4 /* STPDelegateProxy.h */; }; + D0E9BA611F055A4300F079A4 /* STPDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA5F1F055A4300F079A4 /* STPDelegateProxy.m */; }; + D0E9BA631F055AD200F079A4 /* BotPaymentCardInputItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA621F055AD200F079A4 /* BotPaymentCardInputItemNode.swift */; }; + D0E9BA651F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA641F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift */; }; + D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BA661F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift */; }; + D0E9BA911F056F4C00F079A4 /* stp_card_amex@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA691F056F4C00F079A4 /* stp_card_amex@2x.png */; }; + D0E9BA921F056F4C00F079A4 /* stp_card_amex@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6A1F056F4C00F079A4 /* stp_card_amex@3x.png */; }; + D0E9BA931F056F4C00F079A4 /* stp_card_amex_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6B1F056F4C00F079A4 /* stp_card_amex_template@2x.png */; }; + D0E9BA941F056F4C00F079A4 /* stp_card_amex_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6C1F056F4C00F079A4 /* stp_card_amex_template@3x.png */; }; + D0E9BA951F056F4C00F079A4 /* stp_card_applepay@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6D1F056F4C00F079A4 /* stp_card_applepay@2x.png */; }; + D0E9BA961F056F4C00F079A4 /* stp_card_applepay@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6E1F056F4C00F079A4 /* stp_card_applepay@3x.png */; }; + D0E9BA971F056F4C00F079A4 /* stp_card_applepay_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA6F1F056F4C00F079A4 /* stp_card_applepay_template@2x.png */; }; + D0E9BA981F056F4C00F079A4 /* stp_card_applepay_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA701F056F4C00F079A4 /* stp_card_applepay_template@3x.png */; }; + D0E9BA991F056F4C00F079A4 /* stp_card_cvc@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA711F056F4C00F079A4 /* stp_card_cvc@2x.png */; }; + D0E9BA9A1F056F4C00F079A4 /* stp_card_cvc@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA721F056F4C00F079A4 /* stp_card_cvc@3x.png */; }; + D0E9BA9B1F056F4C00F079A4 /* stp_card_cvc_amex@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA731F056F4C00F079A4 /* stp_card_cvc_amex@2x.png */; }; + D0E9BA9C1F056F4C00F079A4 /* stp_card_cvc_amex@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA741F056F4C00F079A4 /* stp_card_cvc_amex@3x.png */; }; + D0E9BA9D1F056F4C00F079A4 /* stp_card_diners@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA751F056F4C00F079A4 /* stp_card_diners@2x.png */; }; + D0E9BA9E1F056F4C00F079A4 /* stp_card_diners@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA761F056F4C00F079A4 /* stp_card_diners@3x.png */; }; + D0E9BA9F1F056F4C00F079A4 /* stp_card_diners_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA771F056F4C00F079A4 /* stp_card_diners_template@2x.png */; }; + D0E9BAA01F056F4C00F079A4 /* stp_card_diners_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA781F056F4C00F079A4 /* stp_card_diners_template@3x.png */; }; + D0E9BAA11F056F4C00F079A4 /* stp_card_discover@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA791F056F4C00F079A4 /* stp_card_discover@2x.png */; }; + D0E9BAA21F056F4C00F079A4 /* stp_card_discover@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7A1F056F4C00F079A4 /* stp_card_discover@3x.png */; }; + D0E9BAA31F056F4C00F079A4 /* stp_card_discover_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7B1F056F4C00F079A4 /* stp_card_discover_template@2x.png */; }; + D0E9BAA41F056F4C00F079A4 /* stp_card_discover_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7C1F056F4C00F079A4 /* stp_card_discover_template@3x.png */; }; + D0E9BAA51F056F4C00F079A4 /* stp_card_form_applepay@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7D1F056F4C00F079A4 /* stp_card_form_applepay@2x.png */; }; + D0E9BAA61F056F4C00F079A4 /* stp_card_form_applepay@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7E1F056F4C00F079A4 /* stp_card_form_applepay@3x.png */; }; + D0E9BAA71F056F4C00F079A4 /* stp_card_form_back@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA7F1F056F4C00F079A4 /* stp_card_form_back@2x.png */; }; + D0E9BAA81F056F4C00F079A4 /* stp_card_form_back@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA801F056F4C00F079A4 /* stp_card_form_back@3x.png */; }; + D0E9BAA91F056F4C00F079A4 /* stp_card_form_front@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA811F056F4C00F079A4 /* stp_card_form_front@2x.png */; }; + D0E9BAAA1F056F4C00F079A4 /* stp_card_form_front@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA821F056F4C00F079A4 /* stp_card_form_front@3x.png */; }; + D0E9BAAB1F056F4C00F079A4 /* stp_card_jcb@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA831F056F4C00F079A4 /* stp_card_jcb@2x.png */; }; + D0E9BAAC1F056F4C00F079A4 /* stp_card_jcb@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA841F056F4C00F079A4 /* stp_card_jcb@3x.png */; }; + D0E9BAAD1F056F4C00F079A4 /* stp_card_jcb_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA851F056F4C00F079A4 /* stp_card_jcb_template@2x.png */; }; + D0E9BAAE1F056F4C00F079A4 /* stp_card_jcb_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA861F056F4C00F079A4 /* stp_card_jcb_template@3x.png */; }; + D0E9BAAF1F056F4C00F079A4 /* stp_card_mastercard@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA871F056F4C00F079A4 /* stp_card_mastercard@2x.png */; }; + D0E9BAB01F056F4C00F079A4 /* stp_card_mastercard@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA881F056F4C00F079A4 /* stp_card_mastercard@3x.png */; }; + D0E9BAB11F056F4C00F079A4 /* stp_card_mastercard_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA891F056F4C00F079A4 /* stp_card_mastercard_template@2x.png */; }; + D0E9BAB21F056F4C00F079A4 /* stp_card_mastercard_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8A1F056F4C00F079A4 /* stp_card_mastercard_template@3x.png */; }; + D0E9BAB31F056F4C00F079A4 /* stp_card_placeholder_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8B1F056F4C00F079A4 /* stp_card_placeholder_template@2x.png */; }; + D0E9BAB41F056F4C00F079A4 /* stp_card_placeholder_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8C1F056F4C00F079A4 /* stp_card_placeholder_template@3x.png */; }; + D0E9BAB51F056F4C00F079A4 /* stp_card_visa@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8D1F056F4C00F079A4 /* stp_card_visa@2x.png */; }; + D0E9BAB61F056F4C00F079A4 /* stp_card_visa@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8E1F056F4C00F079A4 /* stp_card_visa@3x.png */; }; + D0E9BAB71F056F4C00F079A4 /* stp_card_visa_template@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA8F1F056F4C00F079A4 /* stp_card_visa_template@2x.png */; }; + D0E9BAB81F056F4C00F079A4 /* stp_card_visa_template@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0E9BA901F056F4C00F079A4 /* stp_card_visa_template@3x.png */; }; + D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAB91F05735F00F079A4 /* STPPaymentConfiguration.h */; }; + D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BABA1F05735F00F079A4 /* STPPaymentConfiguration.m */; }; + D0E9BABE1F05735F00F079A4 /* STPPaymentConfiguration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BABB1F05735F00F079A4 /* STPPaymentConfiguration+Private.h */; }; + D0E9BAC61F05738600F079A4 /* STPAPIClient.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BABF1F05738600F079A4 /* STPAPIClient.h */; }; + D0E9BAC71F05738600F079A4 /* STPAPIClient.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAC01F05738600F079A4 /* STPAPIClient.m */; }; + D0E9BAC81F05738600F079A4 /* STPAPIClient+ApplePay.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAC11F05738600F079A4 /* STPAPIClient+ApplePay.h */; }; + D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAC21F05738600F079A4 /* STPAPIClient+ApplePay.m */; }; + D0E9BACA1F05738600F079A4 /* STPAPIClient+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAC31F05738600F079A4 /* STPAPIClient+Private.h */; }; + D0E9BACB1F05738600F079A4 /* STPAPIPostRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAC41F05738600F079A4 /* STPAPIPostRequest.h */; }; + D0E9BACC1F05738600F079A4 /* STPAPIPostRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAC51F05738600F079A4 /* STPAPIPostRequest.m */; }; + D0E9BACE1F0573AF00F079A4 /* STPBlocks.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BACD1F0573AF00F079A4 /* STPBlocks.h */; }; + D0E9BAD11F0573C000F079A4 /* STPToken.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BACF1F0573C000F079A4 /* STPToken.h */; }; + D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAD01F0573C000F079A4 /* STPToken.m */; }; + D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAD31F0574D800F079A4 /* PKPayment+Stripe.h */; }; + D0E9BADD1F0574D800F079A4 /* PKPayment+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAD41F0574D800F079A4 /* PKPayment+Stripe.m */; }; + D0E9BADE1F0574D800F079A4 /* STPBackendAPIAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAD51F0574D800F079A4 /* STPBackendAPIAdapter.h */; }; + D0E9BADF1F0574D800F079A4 /* STPDispatchFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAD61F0574D800F079A4 /* STPDispatchFunctions.h */; }; + D0E9BAE01F0574D800F079A4 /* STPDispatchFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAD71F0574D800F079A4 /* STPDispatchFunctions.m */; }; + D0E9BAE11F0574D800F079A4 /* STPBankAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAD81F0574D800F079A4 /* STPBankAccount.h */; }; + D0E9BAE21F0574D800F079A4 /* STPBankAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAD91F0574D800F079A4 /* STPBankAccount.m */; }; + D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BADA1F0574D800F079A4 /* STPBankAccountParams.h */; }; + D0E9BAE41F0574D800F079A4 /* STPBankAccountParams.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BADB1F0574D800F079A4 /* STPBankAccountParams.m */; }; + D0E9BAE71F0574FF00F079A4 /* STPCustomer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E9BAE51F0574FF00F079A4 /* STPCustomer.h */; }; + D0E9BAE81F0574FF00F079A4 /* STPCustomer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0E9BAE61F0574FF00F079A4 /* STPCustomer.m */; }; D0EC6C541EB9F42E00EBF1C3 /* checks.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B9E1EB9F42D00EBF1C3 /* checks.cc */; }; D0EC6C551EB9F42E00EBF1C3 /* stringutils.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6BA51EB9F42D00EBF1C3 /* stringutils.cc */; }; D0EC6C561EB9F42E00EBF1C3 /* audio_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6BA91EB9F42D00EBF1C3 /* audio_util.cc */; }; @@ -205,7 +346,6 @@ 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 */; }; - D0EC6D011EB9F58800EBF1C3 /* OverlayMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B461EB92EB700EBF1C3 /* OverlayMediaItem.swift */; }; D0EC6D021EB9F58800EBF1C3 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; }; D0EC6D031EB9F58800EBF1C3 /* opus_header.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AEA1DECB0FE00220C46 /* opus_header.c */; }; D0EC6D041EB9F58800EBF1C3 /* opusenc.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AED1DECB0FE00220C46 /* opusenc.m */; }; @@ -718,6 +858,10 @@ D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; }; D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -735,7 +879,6 @@ D00370311DA46C06004308D3 /* ItemListTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextWithLabelItem.swift; sourceTree = ""; }; D00ADFDA1EBA2EAF00873D2E /* OngoingCallContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OngoingCallContext.swift; sourceTree = ""; }; D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaManager.swift; sourceTree = ""; }; - D00ADFDE1EBB944900873D2E /* VideoOverlayMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoOverlayMediaItem.swift; sourceTree = ""; }; D00B3F9D1E3A4847003872C3 /* ItemListSectionHeaderItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSectionHeaderItem.swift; sourceTree = ""; }; D00B3F9F1E3A76D4003872C3 /* ItemListSwitchItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSwitchItem.swift; sourceTree = ""; }; D00B3FA11E3A983E003872C3 /* ItemListTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextItem.swift; sourceTree = ""; }; @@ -790,6 +933,7 @@ D01C2AA01E758F90001F6F9A /* NavigateToChatController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigateToChatController.swift; sourceTree = ""; }; D01C2AAA1E75E010001F6F9A /* TwoStepVerificationUnlockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockController.swift; sourceTree = ""; }; D01C2AAC1E768404001F6F9A /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = ""; }; + D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContactsManager.swift; sourceTree = ""; }; D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = ""; }; D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingButton.swift; sourceTree = ""; }; D0215D371E040F53001A0B1E /* InstantPageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNode.swift; sourceTree = ""; }; @@ -837,6 +981,7 @@ D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.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 = ""; }; D03922A61DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerScrubbingNode.swift; sourceTree = ""; }; D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; @@ -855,6 +1000,19 @@ D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatItemGalleryFooterContentNode.swift; sourceTree = ""; }; D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatItemGalleryItemNode.swift; sourceTree = ""; }; D04662801E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOutgoingMessageMedia.swift; sourceTree = ""; }; + D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutControllerNode.swift; sourceTree = ""; }; + D0471B4A1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutHeaderItem.swift; sourceTree = ""; }; + D0471B4E1EFD84600074D609 /* BotCheckoutPriceItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPriceItem.swift; sourceTree = ""; }; + D0471B501EFD872F0074D609 /* CurrencyFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyFormat.swift; sourceTree = ""; }; + D0471B531EFD8ECA0074D609 /* currencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = currencies.json; path = TelegramUI/Resources/currencies.json; sourceTree = ""; }; + D0471B551EFDB40F0074D609 /* BotCheckoutActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutActionButton.swift; sourceTree = ""; }; + D0471B571EFE6D020074D609 /* BotCheckoutInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutInfoController.swift; sourceTree = ""; }; + D0471B591EFE70400074D609 /* BotCheckoutInfoControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutInfoControllerNode.swift; sourceTree = ""; }; + D0471B5B1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentFieldItemNode.swift; sourceTree = ""; }; + D0471B5D1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentHeaderItemNode.swift; sourceTree = ""; }; + D0471B5F1EFEB5A70074D609 /* BotPaymentTextItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentTextItemNode.swift; sourceTree = ""; }; + D0471B611EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentSwitchItemNode.swift; sourceTree = ""; }; + D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentItemNode.swift; sourceTree = ""; }; D04791661E79A22000F18979 /* ItemListStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListStickerPackItem.swift; sourceTree = ""; }; D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = ""; }; D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = ""; }; @@ -1007,6 +1165,11 @@ D0760B231E9D015D00F1F3C4 /* PasscodeOptionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeOptionsController.swift; sourceTree = ""; }; D07827BC1E004A3400071108 /* ChatListSearchItemHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListSearchItemHeader.swift; sourceTree = ""; }; D07827C61E01CD5900071108 /* VerticalListContextResultsChatInputPanelButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputPanelButtonItem.swift; sourceTree = ""; }; + D079FCD81F05A5550038FADE /* BotCheckoutPasswordEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPasswordEntryController.swift; sourceTree = ""; }; + D079FCDC1F05C4F20038FADE /* LocalAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAuth.swift; sourceTree = ""; }; + D079FCDE1F05C9280038FADE /* BotReceiptController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotReceiptController.swift; sourceTree = ""; }; + D079FCE01F05C9380038FADE /* BotReceiptControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotReceiptControllerNode.swift; sourceTree = ""; }; + D079FCE81F06A76C0038FADE /* Notices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notices.swift; sourceTree = ""; }; D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = ""; }; D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = ""; }; D07CFF731DCA207200761F81 /* PeerSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionController.swift; sourceTree = ""; }; @@ -1049,6 +1212,13 @@ 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 = ""; }; + 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 = ""; }; + D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbedGalleryVideoItem.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; }; D0A11BF91E7836C20081CE03 /* ChangePhoneNumberIntroController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberIntroController.swift; sourceTree = ""; }; D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = ""; }; D0A11BFD1E7840A50081CE03 /* ChangePhoneNumberControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberControllerNode.swift; sourceTree = ""; }; @@ -1176,6 +1346,121 @@ D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryViewForLocation.swift; sourceTree = ""; }; D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntriesForView.swift; sourceTree = ""; }; D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedChatHistoryViewTransition.swift; sourceTree = ""; }; + D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentDisclosureItemNode.swift; sourceTree = ""; }; + D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = PhoneCountries.txt; path = TelegramUI/Resources/PhoneCountries.txt; sourceTree = ""; }; + D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPaymentMethodSheet.swift; sourceTree = ""; }; + D0E9BA071F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPaymentShippingOptionSheetController.swift; sourceTree = ""; }; + D0E9BA091F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutWebInteractionController.swift; sourceTree = ""; }; + D0E9BA0B1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutWebInteractionControllerNode.swift; sourceTree = ""; }; + D0E9BA0D1F05574500F079A4 /* STPCardValidationState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardValidationState.h; sourceTree = ""; }; + D0E9BA0E1F05574500F079A4 /* STPCardValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardValidator.h; sourceTree = ""; }; + D0E9BA0F1F05574500F079A4 /* STPCardValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCardValidator.m; sourceTree = ""; }; + D0E9BA101F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentCardTextFieldViewModel.h; sourceTree = ""; }; + D0E9BA111F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentCardTextFieldViewModel.m; sourceTree = ""; }; + D0E9BA121F05574500F079A4 /* STPPaymentCardTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentCardTextField.h; sourceTree = ""; }; + D0E9BA131F05574500F079A4 /* STPPaymentCardTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentCardTextField.m; sourceTree = ""; }; + D0E9BA1C1F05577700F079A4 /* STPCardParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardParams.h; sourceTree = ""; }; + D0E9BA1D1F05577700F079A4 /* STPCardParams.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCardParams.m; sourceTree = ""; }; + D0E9BA1E1F05577700F079A4 /* STPCard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCard.h; sourceTree = ""; }; + D0E9BA1F1F05577700F079A4 /* STPCard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCard.m; sourceTree = ""; }; + D0E9BA241F05578900F079A4 /* STPCardBrand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardBrand.h; sourceTree = ""; }; + D0E9BA261F0557A600F079A4 /* STPFormEncodable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPFormEncodable.h; sourceTree = ""; }; + D0E9BA271F0557A600F079A4 /* STPFormEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPFormEncoder.h; sourceTree = ""; }; + D0E9BA281F0557A600F079A4 /* STPFormEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPFormEncoder.m; sourceTree = ""; }; + D0E9BA2C1F0557D400F079A4 /* STPAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAddress.h; sourceTree = ""; }; + D0E9BA2D1F0557D400F079A4 /* STPAddress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAddress.m; sourceTree = ""; }; + D0E9BA301F05583A00F079A4 /* STPPostalCodeValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPostalCodeValidator.h; sourceTree = ""; }; + D0E9BA311F05583A00F079A4 /* STPPostalCodeValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPostalCodeValidator.m; sourceTree = ""; }; + D0E9BA341F05585000F079A4 /* STPPhoneNumberValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPhoneNumberValidator.h; sourceTree = ""; }; + D0E9BA351F05585000F079A4 /* STPPhoneNumberValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPhoneNumberValidator.m; sourceTree = ""; }; + D0E9BA381F0558E800F079A4 /* NSString+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Stripe.h"; sourceTree = ""; }; + D0E9BA391F0558E800F079A4 /* NSString+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Stripe.m"; sourceTree = ""; }; + D0E9BA3C1F0558FE00F079A4 /* STPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPSource.h; sourceTree = ""; }; + D0E9BA3D1F0558FE00F079A4 /* StripeError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StripeError.h; sourceTree = ""; }; + D0E9BA3E1F0558FE00F079A4 /* StripeError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StripeError.m; sourceTree = ""; }; + D0E9BA421F0559A500F079A4 /* STPAPIResponseDecodable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIResponseDecodable.h; sourceTree = ""; }; + D0E9BA431F0559A500F079A4 /* NSDictionary+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Stripe.h"; sourceTree = ""; }; + D0E9BA441F0559A500F079A4 /* NSDictionary+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Stripe.m"; sourceTree = ""; }; + D0E9BA481F0559B600F079A4 /* STPPaymentMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentMethod.h; sourceTree = ""; }; + D0E9BA4A1F0559C700F079A4 /* NSString+Stripe_CardBrands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Stripe_CardBrands.h"; sourceTree = ""; }; + D0E9BA4B1F0559C700F079A4 /* NSString+Stripe_CardBrands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Stripe_CardBrands.m"; sourceTree = ""; }; + D0E9BA4E1F0559DA00F079A4 /* STPImageLibrary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPImageLibrary.h; sourceTree = ""; }; + D0E9BA4F1F0559DA00F079A4 /* STPImageLibrary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPImageLibrary.m; sourceTree = ""; }; + D0E9BA501F0559DA00F079A4 /* STPImageLibrary+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPImageLibrary+Private.h"; sourceTree = ""; }; + D0E9BA541F055A0B00F079A4 /* STPFormTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPFormTextField.h; sourceTree = ""; }; + D0E9BA551F055A0B00F079A4 /* STPFormTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPFormTextField.m; sourceTree = ""; }; + D0E9BA581F055A2200F079A4 /* STPWeakStrongMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPWeakStrongMacros.h; sourceTree = ""; }; + D0E9BA5A1F055A3300F079A4 /* STPBINRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBINRange.h; sourceTree = ""; }; + D0E9BA5B1F055A3300F079A4 /* STPBINRange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBINRange.m; sourceTree = ""; }; + D0E9BA5E1F055A4300F079A4 /* STPDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPDelegateProxy.h; sourceTree = ""; }; + D0E9BA5F1F055A4300F079A4 /* STPDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPDelegateProxy.m; sourceTree = ""; }; + D0E9BA621F055AD200F079A4 /* BotPaymentCardInputItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentCardInputItemNode.swift; sourceTree = ""; }; + D0E9BA641F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutNativeCardEntryController.swift; sourceTree = ""; }; + D0E9BA661F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutNativeCardEntryControllerNode.swift; sourceTree = ""; }; + D0E9BA691F056F4C00F079A4 /* stp_card_amex@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_amex@2x.png"; sourceTree = ""; }; + D0E9BA6A1F056F4C00F079A4 /* stp_card_amex@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_amex@3x.png"; sourceTree = ""; }; + D0E9BA6B1F056F4C00F079A4 /* stp_card_amex_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_amex_template@2x.png"; sourceTree = ""; }; + D0E9BA6C1F056F4C00F079A4 /* stp_card_amex_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_amex_template@3x.png"; sourceTree = ""; }; + D0E9BA6D1F056F4C00F079A4 /* stp_card_applepay@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_applepay@2x.png"; sourceTree = ""; }; + D0E9BA6E1F056F4C00F079A4 /* stp_card_applepay@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_applepay@3x.png"; sourceTree = ""; }; + D0E9BA6F1F056F4C00F079A4 /* stp_card_applepay_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_applepay_template@2x.png"; sourceTree = ""; }; + D0E9BA701F056F4C00F079A4 /* stp_card_applepay_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_applepay_template@3x.png"; sourceTree = ""; }; + D0E9BA711F056F4C00F079A4 /* stp_card_cvc@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_cvc@2x.png"; sourceTree = ""; }; + D0E9BA721F056F4C00F079A4 /* stp_card_cvc@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_cvc@3x.png"; sourceTree = ""; }; + D0E9BA731F056F4C00F079A4 /* stp_card_cvc_amex@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_cvc_amex@2x.png"; sourceTree = ""; }; + D0E9BA741F056F4C00F079A4 /* stp_card_cvc_amex@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_cvc_amex@3x.png"; sourceTree = ""; }; + D0E9BA751F056F4C00F079A4 /* stp_card_diners@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_diners@2x.png"; sourceTree = ""; }; + D0E9BA761F056F4C00F079A4 /* stp_card_diners@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_diners@3x.png"; sourceTree = ""; }; + D0E9BA771F056F4C00F079A4 /* stp_card_diners_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_diners_template@2x.png"; sourceTree = ""; }; + D0E9BA781F056F4C00F079A4 /* stp_card_diners_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_diners_template@3x.png"; sourceTree = ""; }; + D0E9BA791F056F4C00F079A4 /* stp_card_discover@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_discover@2x.png"; sourceTree = ""; }; + D0E9BA7A1F056F4C00F079A4 /* stp_card_discover@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_discover@3x.png"; sourceTree = ""; }; + D0E9BA7B1F056F4C00F079A4 /* stp_card_discover_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_discover_template@2x.png"; sourceTree = ""; }; + D0E9BA7C1F056F4C00F079A4 /* stp_card_discover_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_discover_template@3x.png"; sourceTree = ""; }; + D0E9BA7D1F056F4C00F079A4 /* stp_card_form_applepay@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_applepay@2x.png"; sourceTree = ""; }; + D0E9BA7E1F056F4C00F079A4 /* stp_card_form_applepay@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_applepay@3x.png"; sourceTree = ""; }; + D0E9BA7F1F056F4C00F079A4 /* stp_card_form_back@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_back@2x.png"; sourceTree = ""; }; + D0E9BA801F056F4C00F079A4 /* stp_card_form_back@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_back@3x.png"; sourceTree = ""; }; + D0E9BA811F056F4C00F079A4 /* stp_card_form_front@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_front@2x.png"; sourceTree = ""; }; + D0E9BA821F056F4C00F079A4 /* stp_card_form_front@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_form_front@3x.png"; sourceTree = ""; }; + D0E9BA831F056F4C00F079A4 /* stp_card_jcb@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb@2x.png"; sourceTree = ""; }; + D0E9BA841F056F4C00F079A4 /* stp_card_jcb@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb@3x.png"; sourceTree = ""; }; + D0E9BA851F056F4C00F079A4 /* stp_card_jcb_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb_template@2x.png"; sourceTree = ""; }; + D0E9BA861F056F4C00F079A4 /* stp_card_jcb_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_jcb_template@3x.png"; sourceTree = ""; }; + D0E9BA871F056F4C00F079A4 /* stp_card_mastercard@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_mastercard@2x.png"; sourceTree = ""; }; + D0E9BA881F056F4C00F079A4 /* stp_card_mastercard@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_mastercard@3x.png"; sourceTree = ""; }; + D0E9BA891F056F4C00F079A4 /* stp_card_mastercard_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_mastercard_template@2x.png"; sourceTree = ""; }; + D0E9BA8A1F056F4C00F079A4 /* stp_card_mastercard_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_mastercard_template@3x.png"; sourceTree = ""; }; + D0E9BA8B1F056F4C00F079A4 /* stp_card_placeholder_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_placeholder_template@2x.png"; sourceTree = ""; }; + D0E9BA8C1F056F4C00F079A4 /* stp_card_placeholder_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_placeholder_template@3x.png"; sourceTree = ""; }; + D0E9BA8D1F056F4C00F079A4 /* stp_card_visa@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_visa@2x.png"; sourceTree = ""; }; + D0E9BA8E1F056F4C00F079A4 /* stp_card_visa@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_visa@3x.png"; sourceTree = ""; }; + D0E9BA8F1F056F4C00F079A4 /* stp_card_visa_template@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_visa_template@2x.png"; sourceTree = ""; }; + D0E9BA901F056F4C00F079A4 /* stp_card_visa_template@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stp_card_visa_template@3x.png"; sourceTree = ""; }; + D0E9BAB91F05735F00F079A4 /* STPPaymentConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentConfiguration.h; sourceTree = ""; }; + D0E9BABA1F05735F00F079A4 /* STPPaymentConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentConfiguration.m; sourceTree = ""; }; + D0E9BABB1F05735F00F079A4 /* STPPaymentConfiguration+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPPaymentConfiguration+Private.h"; sourceTree = ""; }; + D0E9BABF1F05738600F079A4 /* STPAPIClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIClient.h; sourceTree = ""; }; + D0E9BAC01F05738600F079A4 /* STPAPIClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAPIClient.m; sourceTree = ""; }; + D0E9BAC11F05738600F079A4 /* STPAPIClient+ApplePay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPAPIClient+ApplePay.h"; sourceTree = ""; }; + D0E9BAC21F05738600F079A4 /* STPAPIClient+ApplePay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "STPAPIClient+ApplePay.m"; sourceTree = ""; }; + D0E9BAC31F05738600F079A4 /* STPAPIClient+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPAPIClient+Private.h"; sourceTree = ""; }; + D0E9BAC41F05738600F079A4 /* STPAPIPostRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIPostRequest.h; sourceTree = ""; }; + D0E9BAC51F05738600F079A4 /* STPAPIPostRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAPIPostRequest.m; sourceTree = ""; }; + D0E9BACD1F0573AF00F079A4 /* STPBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBlocks.h; sourceTree = ""; }; + D0E9BACF1F0573C000F079A4 /* STPToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPToken.h; sourceTree = ""; }; + D0E9BAD01F0573C000F079A4 /* STPToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPToken.m; sourceTree = ""; }; + D0E9BAD31F0574D800F079A4 /* PKPayment+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PKPayment+Stripe.h"; sourceTree = ""; }; + D0E9BAD41F0574D800F079A4 /* PKPayment+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PKPayment+Stripe.m"; sourceTree = ""; }; + D0E9BAD51F0574D800F079A4 /* STPBackendAPIAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBackendAPIAdapter.h; sourceTree = ""; }; + D0E9BAD61F0574D800F079A4 /* STPDispatchFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPDispatchFunctions.h; sourceTree = ""; }; + D0E9BAD71F0574D800F079A4 /* STPDispatchFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPDispatchFunctions.m; sourceTree = ""; }; + D0E9BAD81F0574D800F079A4 /* STPBankAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBankAccount.h; sourceTree = ""; }; + D0E9BAD91F0574D800F079A4 /* STPBankAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBankAccount.m; sourceTree = ""; }; + D0E9BADA1F0574D800F079A4 /* STPBankAccountParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBankAccountParams.h; sourceTree = ""; }; + D0E9BADB1F0574D800F079A4 /* STPBankAccountParams.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBankAccountParams.m; sourceTree = ""; }; + D0E9BAE51F0574FF00F079A4 /* STPCustomer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCustomer.h; sourceTree = ""; }; + D0E9BAE61F0574FF00F079A4 /* STPCustomer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCustomer.m; sourceTree = ""; }; D0EAE09F1EB21256005296C1 /* StringPluralization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringPluralization.swift; sourceTree = ""; }; D0EAE0A11EB212DE005296C1 /* NumberPluralizationForm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NumberPluralizationForm.h; sourceTree = ""; }; D0EAE0A21EB212DE005296C1 /* NumberPluralizationForm.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NumberPluralizationForm.m; sourceTree = ""; }; @@ -1187,7 +1472,6 @@ D0EC6B401EB8F7D700EBF1C3 /* VoipDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VoipDynamic.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/VoipDynamic.framework"; sourceTree = ""; }; D0EC6B421EB92DF600EBF1C3 /* OverlayMediaController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaController.swift; sourceTree = ""; }; D0EC6B441EB92E5A00EBF1C3 /* OverlayMediaControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaControllerNode.swift; sourceTree = ""; }; - D0EC6B461EB92EB700EBF1C3 /* OverlayMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaItem.swift; sourceTree = ""; }; D0EC6B4A1EB9F42D00EBF1C3 /* AudioInput.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioInput.cpp; sourceTree = ""; }; D0EC6B4B1EB9F42D00EBF1C3 /* AudioInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioInput.h; sourceTree = ""; }; D0EC6B4C1EB9F42D00EBF1C3 /* AudioOutput.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioOutput.cpp; sourceTree = ""; }; @@ -1653,6 +1937,10 @@ D0FC40881D5B8E7500261D9D /* TelegramUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TelegramUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelegramUITests.swift; sourceTree = ""; }; D0FC408F1D5B8E7500261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1660,6 +1948,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */, + D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */, D0B4AF881EC112EE00D51FF6 /* CallKit.framework in Frameworks */, D0EC6EBD1EBA100F00EBF1C3 /* CoreAudio.framework in Frameworks */, D0EC6EA61EB9FC2400EBF1C3 /* libc++.tbd in Frameworks */, @@ -1754,6 +2044,14 @@ name = "Call List"; sourceTree = ""; }; + D01C7EFE1EF9D434008305F1 /* Device Contacts */ = { + isa = PBXGroup; + children = ( + D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */, + ); + name = "Device Contacts"; + sourceTree = ""; + }; D021E0CC1DB4132E00C6B04F /* Input Nodes */ = { isa = PBXGroup; children = ( @@ -1830,6 +2128,23 @@ name = "Accessory Panels"; sourceTree = ""; }; + D0471B471EFD4E920074D609 /* Payment */ = { + isa = PBXGroup; + children = ( + ); + name = Payment; + sourceTree = ""; + }; + D0471B521EFD8EBC0074D609 /* Resources */ = { + isa = PBXGroup; + children = ( + D0E9BA681F056F4C00F079A4 /* Stripe */, + D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */, + D0471B531EFD8ECA0074D609 /* currencies.json */, + ); + name = Resources; + sourceTree = ""; + }; D049EAE01E447AB700A2CD3A /* Stickers */ = { isa = PBXGroup; children = ( @@ -2085,7 +2400,30 @@ D0754D251EEE10A100884F6E /* Bot Payments */ = { isa = PBXGroup; children = ( + D0E9BA1B1F05574800F079A4 /* Stripe */, D0754D261EEE10C800884F6E /* BotCheckoutController.swift */, + D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */, + D0471B4A1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift */, + D0471B4E1EFD84600074D609 /* BotCheckoutPriceItem.swift */, + D0471B551EFDB40F0074D609 /* BotCheckoutActionButton.swift */, + D0471B571EFE6D020074D609 /* BotCheckoutInfoController.swift */, + D0471B591EFE70400074D609 /* BotCheckoutInfoControllerNode.swift */, + D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */, + D0471B5D1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift */, + D0471B5F1EFEB5A70074D609 /* BotPaymentTextItemNode.swift */, + D0471B5B1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift */, + D0471B611EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift */, + D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */, + D0E9BA621F055AD200F079A4 /* BotPaymentCardInputItemNode.swift */, + D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */, + D0E9BA071F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift */, + D0E9BA091F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift */, + D0E9BA0B1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift */, + D0E9BA641F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift */, + D0E9BA661F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift */, + D079FCD81F05A5550038FADE /* BotCheckoutPasswordEntryController.swift */, + D079FCDE1F05C9280038FADE /* BotReceiptController.swift */, + D079FCE01F05C9380038FADE /* BotReceiptControllerNode.swift */, ); name = "Bot Payments"; sourceTree = ""; @@ -2197,6 +2535,8 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D09E63B11F11289A003444CD /* PassKit.framework */, + D09E63AF1F1010FE003444CD /* Contacts.framework */, D0B4AF871EC112ED00D51FF6 /* CallKit.framework */, D0EC6EBC1EBA100F00EBF1C3 /* CoreAudio.framework */, D0EC6E941EB9F5B300EBF1C3 /* MtProtoKitDynamic.framework */, @@ -2265,30 +2605,14 @@ name = "Horizontal List"; sourceTree = ""; }; - D099EA251DE76585001AF5A8 /* Media Manager */ = { + D09E637D1F0E8C66003444CD /* Shared Media Player */ = { isa = PBXGroup; children = ( - D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */, - D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */, - D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */, - D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */, - D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */, - D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, - D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */, - D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */, - D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */, - D0D03B2B1DED9B8900220C46 /* AudioWaveform.swift */, + D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */, + D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */, D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */, - D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */, - D0EC6B421EB92DF600EBF1C3 /* OverlayMediaController.swift */, - D0EC6B441EB92E5A00EBF1C3 /* OverlayMediaControllerNode.swift */, - D0EC6B461EB92EB700EBF1C3 /* OverlayMediaItem.swift */, - D00ADFDE1EBB944900873D2E /* VideoOverlayMediaItem.swift */, - D0D03AE61DECB0D200220C46 /* Audio Recorder */, - D0F69DBC1D6B886C0046BCD6 /* Player */, - D0EC6FF71EBA1DAE00EBF1C3 /* Calls */, ); - name = "Media Manager"; + name = "Shared Media Player"; sourceTree = ""; }; D0AF7C441ED84BB000CD8E0F /* Language Selection */ = { @@ -2632,6 +2956,127 @@ name = "Chat History Node"; sourceTree = ""; }; + D0E9BA1B1F05574800F079A4 /* Stripe */ = { + isa = PBXGroup; + children = ( + D0E9BAE51F0574FF00F079A4 /* STPCustomer.h */, + D0E9BAE61F0574FF00F079A4 /* STPCustomer.m */, + D0E9BAD31F0574D800F079A4 /* PKPayment+Stripe.h */, + D0E9BAD41F0574D800F079A4 /* PKPayment+Stripe.m */, + D0E9BAD51F0574D800F079A4 /* STPBackendAPIAdapter.h */, + D0E9BAD61F0574D800F079A4 /* STPDispatchFunctions.h */, + D0E9BAD71F0574D800F079A4 /* STPDispatchFunctions.m */, + D0E9BAD81F0574D800F079A4 /* STPBankAccount.h */, + D0E9BAD91F0574D800F079A4 /* STPBankAccount.m */, + D0E9BADA1F0574D800F079A4 /* STPBankAccountParams.h */, + D0E9BADB1F0574D800F079A4 /* STPBankAccountParams.m */, + D0E9BACF1F0573C000F079A4 /* STPToken.h */, + D0E9BAD01F0573C000F079A4 /* STPToken.m */, + D0E9BACD1F0573AF00F079A4 /* STPBlocks.h */, + D0E9BABF1F05738600F079A4 /* STPAPIClient.h */, + D0E9BAC01F05738600F079A4 /* STPAPIClient.m */, + D0E9BAC11F05738600F079A4 /* STPAPIClient+ApplePay.h */, + D0E9BAC21F05738600F079A4 /* STPAPIClient+ApplePay.m */, + D0E9BAC31F05738600F079A4 /* STPAPIClient+Private.h */, + D0E9BAC41F05738600F079A4 /* STPAPIPostRequest.h */, + D0E9BAC51F05738600F079A4 /* STPAPIPostRequest.m */, + D0E9BAB91F05735F00F079A4 /* STPPaymentConfiguration.h */, + D0E9BABA1F05735F00F079A4 /* STPPaymentConfiguration.m */, + D0E9BABB1F05735F00F079A4 /* STPPaymentConfiguration+Private.h */, + D0E9BA5E1F055A4300F079A4 /* STPDelegateProxy.h */, + D0E9BA5F1F055A4300F079A4 /* STPDelegateProxy.m */, + D0E9BA5A1F055A3300F079A4 /* STPBINRange.h */, + D0E9BA5B1F055A3300F079A4 /* STPBINRange.m */, + D0E9BA581F055A2200F079A4 /* STPWeakStrongMacros.h */, + D0E9BA541F055A0B00F079A4 /* STPFormTextField.h */, + D0E9BA551F055A0B00F079A4 /* STPFormTextField.m */, + D0E9BA4E1F0559DA00F079A4 /* STPImageLibrary.h */, + D0E9BA4F1F0559DA00F079A4 /* STPImageLibrary.m */, + D0E9BA501F0559DA00F079A4 /* STPImageLibrary+Private.h */, + D0E9BA4A1F0559C700F079A4 /* NSString+Stripe_CardBrands.h */, + D0E9BA4B1F0559C700F079A4 /* NSString+Stripe_CardBrands.m */, + D0E9BA481F0559B600F079A4 /* STPPaymentMethod.h */, + D0E9BA421F0559A500F079A4 /* STPAPIResponseDecodable.h */, + D0E9BA431F0559A500F079A4 /* NSDictionary+Stripe.h */, + D0E9BA441F0559A500F079A4 /* NSDictionary+Stripe.m */, + D0E9BA3C1F0558FE00F079A4 /* STPSource.h */, + D0E9BA3D1F0558FE00F079A4 /* StripeError.h */, + D0E9BA3E1F0558FE00F079A4 /* StripeError.m */, + D0E9BA381F0558E800F079A4 /* NSString+Stripe.h */, + D0E9BA391F0558E800F079A4 /* NSString+Stripe.m */, + D0E9BA341F05585000F079A4 /* STPPhoneNumberValidator.h */, + D0E9BA351F05585000F079A4 /* STPPhoneNumberValidator.m */, + D0E9BA301F05583A00F079A4 /* STPPostalCodeValidator.h */, + D0E9BA311F05583A00F079A4 /* STPPostalCodeValidator.m */, + D0E9BA2C1F0557D400F079A4 /* STPAddress.h */, + D0E9BA2D1F0557D400F079A4 /* STPAddress.m */, + D0E9BA261F0557A600F079A4 /* STPFormEncodable.h */, + D0E9BA271F0557A600F079A4 /* STPFormEncoder.h */, + D0E9BA281F0557A600F079A4 /* STPFormEncoder.m */, + D0E9BA241F05578900F079A4 /* STPCardBrand.h */, + D0E9BA1C1F05577700F079A4 /* STPCardParams.h */, + D0E9BA1D1F05577700F079A4 /* STPCardParams.m */, + D0E9BA1E1F05577700F079A4 /* STPCard.h */, + D0E9BA1F1F05577700F079A4 /* STPCard.m */, + D0E9BA0D1F05574500F079A4 /* STPCardValidationState.h */, + D0E9BA0E1F05574500F079A4 /* STPCardValidator.h */, + D0E9BA0F1F05574500F079A4 /* STPCardValidator.m */, + D0E9BA101F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h */, + D0E9BA111F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m */, + D0E9BA121F05574500F079A4 /* STPPaymentCardTextField.h */, + D0E9BA131F05574500F079A4 /* STPPaymentCardTextField.m */, + ); + name = Stripe; + sourceTree = ""; + }; + D0E9BA681F056F4C00F079A4 /* Stripe */ = { + isa = PBXGroup; + children = ( + D0E9BA691F056F4C00F079A4 /* stp_card_amex@2x.png */, + D0E9BA6A1F056F4C00F079A4 /* stp_card_amex@3x.png */, + D0E9BA6B1F056F4C00F079A4 /* stp_card_amex_template@2x.png */, + D0E9BA6C1F056F4C00F079A4 /* stp_card_amex_template@3x.png */, + D0E9BA6D1F056F4C00F079A4 /* stp_card_applepay@2x.png */, + D0E9BA6E1F056F4C00F079A4 /* stp_card_applepay@3x.png */, + D0E9BA6F1F056F4C00F079A4 /* stp_card_applepay_template@2x.png */, + D0E9BA701F056F4C00F079A4 /* stp_card_applepay_template@3x.png */, + D0E9BA711F056F4C00F079A4 /* stp_card_cvc@2x.png */, + D0E9BA721F056F4C00F079A4 /* stp_card_cvc@3x.png */, + D0E9BA731F056F4C00F079A4 /* stp_card_cvc_amex@2x.png */, + D0E9BA741F056F4C00F079A4 /* stp_card_cvc_amex@3x.png */, + D0E9BA751F056F4C00F079A4 /* stp_card_diners@2x.png */, + D0E9BA761F056F4C00F079A4 /* stp_card_diners@3x.png */, + D0E9BA771F056F4C00F079A4 /* stp_card_diners_template@2x.png */, + D0E9BA781F056F4C00F079A4 /* stp_card_diners_template@3x.png */, + D0E9BA791F056F4C00F079A4 /* stp_card_discover@2x.png */, + D0E9BA7A1F056F4C00F079A4 /* stp_card_discover@3x.png */, + D0E9BA7B1F056F4C00F079A4 /* stp_card_discover_template@2x.png */, + D0E9BA7C1F056F4C00F079A4 /* stp_card_discover_template@3x.png */, + D0E9BA7D1F056F4C00F079A4 /* stp_card_form_applepay@2x.png */, + D0E9BA7E1F056F4C00F079A4 /* stp_card_form_applepay@3x.png */, + D0E9BA7F1F056F4C00F079A4 /* stp_card_form_back@2x.png */, + D0E9BA801F056F4C00F079A4 /* stp_card_form_back@3x.png */, + D0E9BA811F056F4C00F079A4 /* stp_card_form_front@2x.png */, + D0E9BA821F056F4C00F079A4 /* stp_card_form_front@3x.png */, + D0E9BA831F056F4C00F079A4 /* stp_card_jcb@2x.png */, + D0E9BA841F056F4C00F079A4 /* stp_card_jcb@3x.png */, + D0E9BA851F056F4C00F079A4 /* stp_card_jcb_template@2x.png */, + D0E9BA861F056F4C00F079A4 /* stp_card_jcb_template@3x.png */, + D0E9BA871F056F4C00F079A4 /* stp_card_mastercard@2x.png */, + D0E9BA881F056F4C00F079A4 /* stp_card_mastercard@3x.png */, + D0E9BA891F056F4C00F079A4 /* stp_card_mastercard_template@2x.png */, + D0E9BA8A1F056F4C00F079A4 /* stp_card_mastercard_template@3x.png */, + D0E9BA8B1F056F4C00F079A4 /* stp_card_placeholder_template@2x.png */, + D0E9BA8C1F056F4C00F079A4 /* stp_card_placeholder_template@3x.png */, + D0E9BA8D1F056F4C00F079A4 /* stp_card_visa@2x.png */, + D0E9BA8E1F056F4C00F079A4 /* stp_card_visa@3x.png */, + D0E9BA8F1F056F4C00F079A4 /* stp_card_visa_template@2x.png */, + D0E9BA901F056F4C00F079A4 /* stp_card_visa_template@3x.png */, + ); + name = Stripe; + path = TelegramUI/Resources/Stripe; + sourceTree = ""; + }; D0EC6B391EB8CF1E00EBF1C3 /* Call */ = { isa = PBXGroup; children = ( @@ -3366,7 +3811,29 @@ D0F69DBB1D6B88330046BCD6 /* Media */ = { isa = PBXGroup; children = ( - D099EA251DE76585001AF5A8 /* Media Manager */, + D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */, + D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */, + D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */, + D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */, + D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */, + D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */, + D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */, + D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.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 */, + D0F69DBC1D6B886C0046BCD6 /* Player */, + D0EC6FF71EBA1DAE00EBF1C3 /* Calls */, D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */, D0F69E9D1D6B8E240046BCD6 /* Resources */, D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */, @@ -3466,6 +3933,7 @@ D0F69DE61D6B8A4E0046BCD6 /* Controllers */ = { isa = PBXGroup; children = ( + D0471B471EFD4E920074D609 /* Payment */, D0F69DE71D6B8A590046BCD6 /* Authorization */, D05174C11EAE582A00A1BF36 /* Root */, D0F69DF61D6B8A720046BCD6 /* Chat List */, @@ -3671,6 +4139,7 @@ D0F69E5D1D6B8BF90046BCD6 /* ChatImageGalleryItem.swift */, D0F69E5E1D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift */, D0F69E5F1D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift */, + D09E63A31F0FAB91003444CD /* EmbedGalleryVideoItem.swift */, D0F69E601D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift */, D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */, D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */, @@ -3769,6 +4238,10 @@ D01C2AAC1E768404001F6F9A /* Markdown.swift */, D0F3A8AA1E82D83E00B4C64C /* TelegramAccountAuxiliaryMethods.swift */, D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */, + D0471B501EFD872F0074D609 /* CurrencyFormat.swift */, + D079FCDC1F05C4F20038FADE /* LocalAuth.swift */, + D079FCE81F06A76C0038FADE /* Notices.swift */, + D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */, ); name = Utils; sourceTree = ""; @@ -3825,6 +4298,7 @@ D0EC6B481EB9F3E800EBF1C3 /* libtgvoip */, D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */, D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */, + D0471B521EFD8EBC0074D609 /* Resources */, D073CE611DCBBE09007511FD /* Sounds */, D0FC40811D5B8E7400261D9D /* TelegramUI */, D0FC408C1D5B8E7500261D9D /* TelegramUITests */, @@ -3849,6 +4323,7 @@ D07551891DDA4C7C0073E051 /* Legacy Components */, D0F69E911D6B8C8E0046BCD6 /* Utils */, D0B4AF891EC1132400D51FF6 /* Calls */, + D01C7EFE1EF9D434008305F1 /* Device Contacts */, D096A4601EA681720000A7AE /* Presentation Data */, D087750A1E3E7A6D00A97350 /* Settings */, D0F69DBB1D6B88330046BCD6 /* Media */, @@ -3878,12 +4353,51 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + D0E9BA221F05577700F079A4 /* STPCard.h in Headers */, + D0E9BA591F055A2200F079A4 /* STPWeakStrongMacros.h in Headers */, + D0E9BADE1F0574D800F079A4 /* STPBackendAPIAdapter.h in Headers */, + D0E9BAD11F0573C000F079A4 /* STPToken.h in Headers */, + D0E9BAE71F0574FF00F079A4 /* STPCustomer.h in Headers */, + D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */, + D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */, + D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */, + D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */, + D0E9BAE11F0574D800F079A4 /* STPBankAccount.h in Headers */, D0EC6FE91EBA15D500EBF1C3 /* signal_processing_library.h in Headers */, + D0E9BACE1F0573AF00F079A4 /* STPBlocks.h in Headers */, + D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */, + D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */, D0EC6FE81EBA138700EBF1C3 /* ooura_fft.h in Headers */, + D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */, + D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */, + D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */, + D0E9BA291F0557A600F079A4 /* STPFormEncodable.h in Headers */, + D0E9BA141F05574500F079A4 /* STPCardValidationState.h in Headers */, + D0E9BA461F0559A500F079A4 /* NSDictionary+Stripe.h in Headers */, + D0E9BAC61F05738600F079A4 /* STPAPIClient.h in Headers */, D00ADFD91EBA2E9D00873D2E /* OngoingCallThreadLocalContext.h in Headers */, + D0E9BA531F0559DA00F079A4 /* STPImageLibrary+Private.h in Headers */, + D0E9BA601F055A4300F079A4 /* STPDelegateProxy.h in Headers */, + D0E9BADF1F0574D800F079A4 /* STPDispatchFunctions.h in Headers */, + D0E9BACB1F05738600F079A4 /* STPAPIPostRequest.h in Headers */, D0EC6CA91EB9F4CC00EBF1C3 /* TelegramUI.h in Headers */, + D0E9BA561F055A0B00F079A4 /* STPFormTextField.h in Headers */, D0F0AAEA1EC254A8005EE2A5 /* NumberPluralizationForm.h in Headers */, + D0E9BABE1F05735F00F079A4 /* STPPaymentConfiguration+Private.h in Headers */, + D0E9BACA1F05738600F079A4 /* STPAPIClient+Private.h in Headers */, + D0E9BA251F05578900F079A4 /* STPCardBrand.h in Headers */, + D0E9BAC81F05738600F079A4 /* STPAPIClient+ApplePay.h in Headers */, D0F0AAE91EC22658005EE2A5 /* SecretChatKeyVisualization.h in Headers */, + D0E9BA451F0559A500F079A4 /* STPAPIResponseDecodable.h in Headers */, + D0E9BA201F05577700F079A4 /* STPCardParams.h in Headers */, + D0E9BA151F05574500F079A4 /* STPCardValidator.h in Headers */, + D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */, + D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */, + D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */, + D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */, + D0E9BA2E1F0557D400F079A4 /* STPAddress.h in Headers */, + D0E9BA5C1F055A3300F079A4 /* STPBINRange.h in Headers */, + D0E9BA3A1F0558E800F079A4 /* NSString+Stripe.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3972,8 +4486,50 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0E9BAA21F056F4C00F079A4 /* stp_card_discover@3x.png in Resources */, + D0E9BAB01F056F4C00F079A4 /* stp_card_mastercard@3x.png in Resources */, + D0E9BAA31F056F4C00F079A4 /* stp_card_discover_template@2x.png in Resources */, + D0E9BAB51F056F4C00F079A4 /* stp_card_visa@2x.png in Resources */, + D0E9BA941F056F4C00F079A4 /* stp_card_amex_template@3x.png in Resources */, + D0E9BA961F056F4C00F079A4 /* stp_card_applepay@3x.png in Resources */, + D0E9BA9A1F056F4C00F079A4 /* stp_card_cvc@3x.png in Resources */, + D0E9BA921F056F4C00F079A4 /* stp_card_amex@3x.png in Resources */, + D0E9BA9F1F056F4C00F079A4 /* stp_card_diners_template@2x.png in Resources */, + D0E9BA9E1F056F4C00F079A4 /* stp_card_diners@3x.png in Resources */, D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */, + D0E9BAAD1F056F4C00F079A4 /* stp_card_jcb_template@2x.png in Resources */, + D0E9BAB71F056F4C00F079A4 /* stp_card_visa_template@2x.png in Resources */, + D0E9BA951F056F4C00F079A4 /* stp_card_applepay@2x.png in Resources */, + D0E9BAA01F056F4C00F079A4 /* stp_card_diners_template@3x.png in Resources */, + D0E9BAAA1F056F4C00F079A4 /* stp_card_form_front@3x.png in Resources */, + D0E9BA971F056F4C00F079A4 /* stp_card_applepay_template@2x.png in Resources */, + D0E9BAB41F056F4C00F079A4 /* stp_card_placeholder_template@3x.png in Resources */, + D0E9BAA71F056F4C00F079A4 /* stp_card_form_back@2x.png in Resources */, + D0E9BAB11F056F4C00F079A4 /* stp_card_mastercard_template@2x.png in Resources */, + D0E9BA9D1F056F4C00F079A4 /* stp_card_diners@2x.png in Resources */, + D0E9BAAF1F056F4C00F079A4 /* stp_card_mastercard@2x.png in Resources */, + D0E9BAAC1F056F4C00F079A4 /* stp_card_jcb@3x.png in Resources */, D03E838F1EC10FE5001A6ED9 /* Info.plist in Resources */, + D0E9BA911F056F4C00F079A4 /* stp_card_amex@2x.png in Resources */, + D0E9BA931F056F4C00F079A4 /* stp_card_amex_template@2x.png in Resources */, + D0E9BAA91F056F4C00F079A4 /* stp_card_form_front@2x.png in Resources */, + D0E9BAA41F056F4C00F079A4 /* stp_card_discover_template@3x.png in Resources */, + D0E9BAA81F056F4C00F079A4 /* stp_card_form_back@3x.png in Resources */, + D0E9BAA11F056F4C00F079A4 /* stp_card_discover@2x.png in Resources */, + D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */, + D0E9BAB31F056F4C00F079A4 /* stp_card_placeholder_template@2x.png in Resources */, + D0E9BAAE1F056F4C00F079A4 /* stp_card_jcb_template@3x.png in Resources */, + D0E9BAAB1F056F4C00F079A4 /* stp_card_jcb@2x.png in Resources */, + D0E9BA9C1F056F4C00F079A4 /* stp_card_cvc_amex@3x.png in Resources */, + D0E9BA991F056F4C00F079A4 /* stp_card_cvc@2x.png in Resources */, + D0471B541EFD8ECA0074D609 /* currencies.json in Resources */, + D0E9BAB21F056F4C00F079A4 /* stp_card_mastercard_template@3x.png in Resources */, + D0E9BA981F056F4C00F079A4 /* stp_card_applepay_template@3x.png in Resources */, + D0E9BAA51F056F4C00F079A4 /* stp_card_form_applepay@2x.png in Resources */, + D0E9BAB81F056F4C00F079A4 /* stp_card_visa_template@3x.png in Resources */, + D0E9BA9B1F056F4C00F079A4 /* stp_card_cvc_amex@2x.png in Resources */, + D0E9BAB61F056F4C00F079A4 /* stp_card_visa@3x.png in Resources */, + D0E9BAA61F056F4C00F079A4 /* stp_card_form_applepay@3x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3992,6 +4548,7 @@ buildActionMask = 2147483647; files = ( D0EC6CAE1EB9F58800EBF1C3 /* animations.c in Sources */, + D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */, D0EC6CAF1EB9F58800EBF1C3 /* buffer.c in Sources */, D0EC6FF01EBA18EB00EBF1C3 /* VoIPServerConfig.cpp in Sources */, D0EC6CB01EB9F58800EBF1C3 /* objects.c in Sources */, @@ -4002,14 +4559,17 @@ D0EC6CB41EB9F58800EBF1C3 /* timing.c in Sources */, D0EC6CB51EB9F58800EBF1C3 /* platform_log.c in Sources */, D0EC6CB61EB9F58800EBF1C3 /* RMGeometry.m in Sources */, + D079FCDD1F05C4F20038FADE /* LocalAuth.swift in Sources */, D0EC6CB71EB9F58800EBF1C3 /* RMIntroPageView.m in Sources */, D0EC6FCB1EBA135100EBF1C3 /* dot_product_with_scale.c in Sources */, D0EC6CB81EB9F58800EBF1C3 /* RMIntroViewController.m in Sources */, D0EC6CB91EB9F58800EBF1C3 /* RMLoginViewController.m in Sources */, + D0E9BA631F055AD200F079A4 /* BotPaymentCardInputItemNode.swift in Sources */, D0EC6CBA1EB9F58800EBF1C3 /* RMRootViewController.m in Sources */, D0EC6CBB1EB9F58800EBF1C3 /* texture_helper.m in Sources */, D0EC6CBC1EB9F58800EBF1C3 /* LegacyController.swift in Sources */, D0EC6CBD1EB9F58800EBF1C3 /* LegacyControllerNode.swift in Sources */, + D079FCE91F06A76C0038FADE /* Notices.swift in Sources */, D0EC6EAF1EBA0FBB00EBF1C3 /* MediaStreamItf.cpp in Sources */, D0C0B5B71EE1DEF1000F4D2C /* ThemeGridControllerItem.swift in Sources */, D0EC6CBE1EB9F58800EBF1C3 /* TelegramInitializeLegacyComponents.swift in Sources */, @@ -4019,6 +4579,7 @@ D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */, D0EC6CC11EB9F58800EBF1C3 /* LegacyCamera.swift in Sources */, D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */, + D0E9BAC71F05738600F079A4 /* STPAPIClient.m in Sources */, D0EC6FEC1EBA182B00EBF1C3 /* aec_core_sse2.cc in Sources */, D0EC6FC81EBA135100EBF1C3 /* cross_correlation.c in Sources */, D0EC6FB11EBA112600EBF1C3 /* ooura_fft_neon.cc in Sources */, @@ -4035,6 +4596,7 @@ D0EC6CCB1EB9F58800EBF1C3 /* ApplicationSpecificData.swift in Sources */, D0EC6FB21EBA114200EBF1C3 /* aec_core.cc in Sources */, D0EC6FE01EBA135100EBF1C3 /* resample_fractional.c in Sources */, + D09E637F1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift in Sources */, D0EC6CCC1EB9F58800EBF1C3 /* ServiceSoundManager.swift in Sources */, D0EC6FCC1EBA135100EBF1C3 /* downsample_fast.c in Sources */, D0EC6CCD1EB9F58800EBF1C3 /* DeclareEncodables.swift in Sources */, @@ -4043,18 +4605,24 @@ D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */, D0EC6EA71EBA0FB000EBF1C3 /* BlockingQueue.cpp in Sources */, D0EC6CD01EB9F58800EBF1C3 /* PerformanceSpinner.swift in Sources */, + D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */, D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */, D0EC6FE31EBA135100EBF1C3 /* spl_sqrt.c in Sources */, + D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */, D0EC6CD21EB9F58800EBF1C3 /* HapticFeedback.swift in Sources */, D0EC6CD31EB9F58800EBF1C3 /* GenerateTextEntities.swift in Sources */, D0EC6CD41EB9F58800EBF1C3 /* StringWithAppliedEntities.swift in Sources */, D0EC6CD51EB9F58800EBF1C3 /* StoredMessageFromSearchPeer.swift in Sources */, + D0471B5E1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift in Sources */, D0EC6CD61EB9F58800EBF1C3 /* PreferencesKeys.swift in Sources */, D0EC6CD71EB9F58800EBF1C3 /* EmojiUtils.swift in Sources */, D0EC6CD81EB9F58800EBF1C3 /* ShakeAnimation.swift in Sources */, D0EC6CD91EB9F58800EBF1C3 /* ValidateAddressNameInteractive.swift in Sources */, + D0471B5A1EFE70400074D609 /* BotCheckoutInfoControllerNode.swift in Sources */, D0EC6CDA1EB9F58800EBF1C3 /* NumericFormat.swift in Sources */, D0EC6CDB1EB9F58800EBF1C3 /* Markdown.swift in Sources */, + D0471B641EFEB5CB0074D609 /* BotPaymentItemNode.swift in Sources */, + D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */, D0EC6CDC1EB9F58800EBF1C3 /* TelegramAccountAuxiliaryMethods.swift in Sources */, D01BAA1A1ECC8E0D00295217 /* CallListControllerNode.swift in Sources */, D0EC6FDF1EBA135100EBF1C3 /* resample_by_2_internal.c in Sources */, @@ -4091,14 +4659,17 @@ D0EC6EB51EBA0FC100EBF1C3 /* Resampler.cpp in Sources */, D0EC6CF41EB9F58800EBF1C3 /* ManagedMediaId.swift in Sources */, D0EC6FD41EBA135100EBF1C3 /* ilbc_specific_functions.c in Sources */, + D0471B601EFEB5A70074D609 /* BotPaymentTextItemNode.swift in Sources */, D0EC6EB11EBA0FBB00EBF1C3 /* OpusDecoder.cpp in Sources */, D0EC6CF51EB9F58800EBF1C3 /* PeerMessageManagedMediaId.swift in Sources */, + D0E9BA521F0559DA00F079A4 /* STPImageLibrary.m in Sources */, D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */, D0EC6EB01EBA0FBB00EBF1C3 /* NetworkSocket.cpp in Sources */, D04B4D661EEA993A00711AF6 /* LegacyLocationController.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 */, D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */, D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */, @@ -4107,9 +4678,10 @@ D0EC6CFE1EB9F58800EBF1C3 /* PeerMediaAudioPlaylist.swift in Sources */, D0EC6CFF1EB9F58800EBF1C3 /* OverlayMediaController.swift in Sources */, D0EC6D001EB9F58800EBF1C3 /* OverlayMediaControllerNode.swift in Sources */, - D0EC6D011EB9F58800EBF1C3 /* OverlayMediaItem.swift in Sources */, D0EC6D021EB9F58800EBF1C3 /* diag_range.c in Sources */, + D0E9BA1A1F05574500F079A4 /* STPPaymentCardTextField.m in Sources */, D0EC6D031EB9F58800EBF1C3 /* opus_header.c in Sources */, + D0E9BA371F05585000F079A4 /* STPPhoneNumberValidator.m in Sources */, D0EC6D041EB9F58800EBF1C3 /* opusenc.m in Sources */, D0EC6D051EB9F58800EBF1C3 /* picture.c in Sources */, D0EC6D061EB9F58800EBF1C3 /* wav_io.c in Sources */, @@ -4136,6 +4708,7 @@ D0EC6D171EB9F58800EBF1C3 /* FFMpegAudioFrameDecoder.swift in Sources */, D0EC6D181EB9F58800EBF1C3 /* FFMpegMediaFrameSource.swift in Sources */, D0EC6D191EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContext.swift in Sources */, + D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */, D0EC6FF61EBA195F00EBF1C3 /* TGLogWrapper.m in Sources */, D0EC6EAD1EBA0FBB00EBF1C3 /* JitterBuffer.cpp in Sources */, D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, @@ -4159,12 +4732,16 @@ D0EC6D271EB9F58800EBF1C3 /* FetchResource.swift in Sources */, D0EC6FDE1EBA135100EBF1C3 /* resample_by_2.c in Sources */, D0EC6D281EB9F58800EBF1C3 /* MediaResources.swift in Sources */, + D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */, D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */, D0EC6FA51EBA111500EBF1C3 /* audio_util.cc in Sources */, D0EC6FFD1EBA1F2400EBF1C3 /* OngoingCallThreadLocalContext.mm in Sources */, + D0E9BAE21F0574D800F079A4 /* STPBankAccount.m in Sources */, D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */, + D0E9BA471F0559A500F079A4 /* NSDictionary+Stripe.m in Sources */, D0EC6D2A1EB9F58800EBF1C3 /* FetchPhotoLibraryImageResource.swift in Sources */, D0EC6FDB1EBA135100EBF1C3 /* refl_coef_to_lpc.c in Sources */, + D0E9BAE01F0574D800F079A4 /* STPDispatchFunctions.m in Sources */, D0EC6D2B1EB9F58800EBF1C3 /* FileMediaResourceStatus.swift in Sources */, D0EC6FE51EBA135100EBF1C3 /* splitting_filter_impl.c in Sources */, D0EC6D2C1EB9F58800EBF1C3 /* TouchDownGestureRecognizer.swift in Sources */, @@ -4190,8 +4767,10 @@ D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */, D0EC6D3E1EB9F58800EBF1C3 /* TelegramController.swift in Sources */, D0EC6D3F1EB9F58800EBF1C3 /* MediaNavigationAccessoryPanel.swift in Sources */, + D0E9BA3B1F0558E800F079A4 /* NSString+Stripe.m in Sources */, D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */, D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */, + D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */, D0EC6D421EB9F58800EBF1C3 /* MediaNavigationAccessoryItemListNode.swift in Sources */, D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */, D0EC6D4B1EB9F58800EBF1C3 /* ChatListNode.swift in Sources */, @@ -4213,6 +4792,7 @@ D0EC6D5B1EB9F58800EBF1C3 /* ListMessageNode.swift in Sources */, D0EC6D5C1EB9F58800EBF1C3 /* ListMessageFileItemNode.swift in Sources */, D0EC6FA91EBA111500EBF1C3 /* wav_file.cc in Sources */, + D0471B561EFDB40F0074D609 /* BotCheckoutActionButton.swift in Sources */, D0EC6FBD1EBA132B00EBF1C3 /* noise_suppression_x.c in Sources */, D0EC6D5D1EB9F58800EBF1C3 /* ListMessageSnippetItemNode.swift in Sources */, D0EC6D5E1EB9F58800EBF1C3 /* ListMessageHoleItem.swift in Sources */, @@ -4221,6 +4801,7 @@ D0EC6D601EB9F58800EBF1C3 /* GridHoleItem.swift in Sources */, D0EC6D611EB9F58800EBF1C3 /* GridMessageSelectionNode.swift in Sources */, D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */, + D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */, D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */, D0EC6D621EB9F58800EBF1C3 /* ContactListNode.swift in Sources */, D0EC6D631EB9F58800EBF1C3 /* ContactListActionItem.swift in Sources */, @@ -4241,11 +4822,13 @@ D0EC6FAB1EBA112600EBF1C3 /* splitting_filter.cc in Sources */, D0EC6D6F1EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryController.swift in Sources */, D0EC6D701EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, + D0E9BA4D1F0559C700F079A4 /* NSString+Stripe_CardBrands.m in Sources */, D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */, D0EC6FBB1EBA114200EBF1C3 /* digital_agc.c in Sources */, D0EC6D711EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryController.swift in Sources */, D0EC6D721EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryControllerNode.swift in Sources */, D0EC6D731EB9F58800EBF1C3 /* AuthorizationSequenceSignUpController.swift in Sources */, + D0E9BA5D1F055A3300F079A4 /* STPBINRange.m in Sources */, D0EC6D741EB9F58800EBF1C3 /* AuthorizationSequenceSignUpControllerNode.swift in Sources */, D0EC6FE21EBA135100EBF1C3 /* spl_inl.c in Sources */, D0EC6FE41EBA135100EBF1C3 /* spl_sqrt_floor.c in Sources */, @@ -4254,11 +4837,14 @@ D0EC6D771EB9F58800EBF1C3 /* ChatListControllerNode.swift in Sources */, D0EC6D781EB9F58800EBF1C3 /* NetworkStatusTitleView.swift in Sources */, D0EC6FAC1EBA112600EBF1C3 /* three_band_filter_bank.cc in Sources */, + D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */, D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */, D0EC6D791EB9F58800EBF1C3 /* ChatListTitleLockView.swift in Sources */, D0EC6D7A1EB9F58800EBF1C3 /* ChatListSearchContainerNode.swift in Sources */, + D0E9BACC1F05738600F079A4 /* STPAPIPostRequest.m in Sources */, D0EC6D7B1EB9F58800EBF1C3 /* ChatListRecentPeersListItem.swift in Sources */, D0EC6D7C1EB9F58800EBF1C3 /* HorizontalPeerItem.swift in Sources */, + D0E9BADD1F0574D800F079A4 /* PKPayment+Stripe.m in Sources */, D0EC6D7D1EB9F58800EBF1C3 /* ChatListSearchRecentPeersNode.swift in Sources */, D0EC6D7E1EB9F58800EBF1C3 /* ChatListSearchItemHeader.swift in Sources */, D0EC6D7F1EB9F58800EBF1C3 /* HashtagSearchController.swift in Sources */, @@ -4266,6 +4852,7 @@ D0EC6D811EB9F58800EBF1C3 /* ChatController.swift in Sources */, D0EC6D821EB9F58800EBF1C3 /* ChatControllerInteraction.swift in Sources */, D0EC6D831EB9F58800EBF1C3 /* ChatControllerNode.swift in Sources */, + D0E9BA231F05577700F079A4 /* STPCard.m in Sources */, D0EC6D841EB9F58800EBF1C3 /* ChatHistoryEntry.swift in Sources */, D0EC6D851EB9F58800EBF1C3 /* ChatHistoryLocation.swift in Sources */, D0EC6D861EB9F58800EBF1C3 /* ChatAvatarNavigationNode.swift in Sources */, @@ -4278,8 +4865,10 @@ D0EC6D8D1EB9F58800EBF1C3 /* ChatMessageActionItemNode.swift in Sources */, D0EC6D8E1EB9F58800EBF1C3 /* ChatMessageAvatarAccessoryItem.swift in Sources */, D0EC6D8F1EB9F58800EBF1C3 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, + D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */, D0EC6D911EB9F58900EBF1C3 /* ChatMessageBubbleItemNode.swift in Sources */, + D0471B511EFD872F0074D609 /* CurrencyFormat.swift in Sources */, D0EC6D921EB9F58900EBF1C3 /* ChatMessageDateAndStatusNode.swift in Sources */, D0EC6D931EB9F58900EBF1C3 /* ChatMessageFileBubbleContentNode.swift in Sources */, D0EC6D941EB9F58900EBF1C3 /* ChatMessageForwardInfoNode.swift in Sources */, @@ -4293,14 +4882,18 @@ D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, D0EC6D991EB9F58900EBF1C3 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0EC6D9A1EB9F58900EBF1C3 /* ChatMessageReplyInfoNode.swift in Sources */, + D0FE4DE41F0AEBB900E8A0B3 /* SharedVideoContextManager.swift in Sources */, D0EC6D9B1EB9F58900EBF1C3 /* ChatMessageStickerItemNode.swift in Sources */, D0EC6D9C1EB9F58900EBF1C3 /* ChatMessageInstantVideoItemNode.swift in Sources */, D0EC6D9D1EB9F58900EBF1C3 /* ChatMessageTextBubbleContentNode.swift in Sources */, + D0E9BA2B1F0557A600F079A4 /* STPFormEncoder.m in Sources */, D01BAA1C1ECC92F700295217 /* CallListViewTransition.swift in Sources */, D0EC6D9E1EB9F58900EBF1C3 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */, D0EC6D9F1EB9F58900EBF1C3 /* ChatUnreadItem.swift in Sources */, + D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */, D0EC6DA01EB9F58900EBF1C3 /* ChatHoleItem.swift in Sources */, + D09E63A21F0FA723003444CD /* EmbedVideoNode.swift in Sources */, D0EC6DA11EB9F58900EBF1C3 /* ChatMessageSelectionNode.swift in Sources */, D0EC6DA21EB9F58900EBF1C3 /* ChatMessageBubbleImages.swift in Sources */, D0EC6DA31EB9F58900EBF1C3 /* ChatMessageDateHeader.swift in Sources */, @@ -4309,6 +4902,7 @@ D0EC6DA51EB9F58900EBF1C3 /* ChatBotInfoItem.swift in Sources */, D0EC6EB81EBA0FD000EBF1C3 /* AudioOutputAudioUnit.cpp in Sources */, D0EC6DA61EB9F58900EBF1C3 /* ChatEmptyItem.swift in Sources */, + D0E9BAE41F0574D800F079A4 /* STPBankAccountParams.m in Sources */, D0EC6DA71EB9F58900EBF1C3 /* ChatMessageBackground.swift in Sources */, D0F0AAE01EC1E12C005EE2A5 /* PresentationCall.swift in Sources */, D0EC6DA81EB9F58900EBF1C3 /* ChatInterfaceState.swift in Sources */, @@ -4324,6 +4918,7 @@ D0EC6DB01EB9F58900EBF1C3 /* ChatInterfaceInputContextPanels.swift in Sources */, D0EC6DB11EB9F58900EBF1C3 /* ChatInterfaceInputNodes.swift in Sources */, D0EC6DB21EB9F58900EBF1C3 /* ChatInterfaceTitlePanelNodes.swift in Sources */, + D0E9BA411F0558FE00F079A4 /* StripeError.m in Sources */, D0EC6DB31EB9F58900EBF1C3 /* ChatInterfaceStateContextQueries.swift in Sources */, D0EC6DB41EB9F58900EBF1C3 /* AccessoryPanelNode.swift in Sources */, D0EC6FEE1EBA186800EBF1C3 /* apm_data_dumper.cc in Sources */, @@ -4333,10 +4928,12 @@ D0EC6DB81EB9F58900EBF1C3 /* WebpagePreviewAccessoryPanelNode.swift in Sources */, D0EC6DB91EB9F58900EBF1C3 /* ChatInputNode.swift in Sources */, D0EC6DBA1EB9F58900EBF1C3 /* ChatMediaInputNode.swift in Sources */, + D0E9BA2F1F0557D400F079A4 /* STPAddress.m in Sources */, D0EC6FED1EBA184A00EBF1C3 /* cpu_features.cc in Sources */, D0EC6DBB1EB9F58900EBF1C3 /* ChatMediaInputStickerPane.swift in Sources */, D0EC6DBC1EB9F58900EBF1C3 /* ChatMediaInputGifPane.swift in Sources */, D0EC6DBD1EB9F58900EBF1C3 /* ChatMediaInputPanelEntries.swift in Sources */, + D0471B4F1EFD84600074D609 /* BotCheckoutPriceItem.swift in Sources */, D00ADFDB1EBA2EAF00873D2E /* OngoingCallContext.swift in Sources */, D0EC6DBE1EB9F58900EBF1C3 /* ChatMediaInputGridEntries.swift in Sources */, D0EC6DBF1EB9F58900EBF1C3 /* ChatMediaInputRecentStickerPacksItem.swift in Sources */, @@ -4347,7 +4944,9 @@ D0EC6DC31EB9F58900EBF1C3 /* ChatMediaInputStickerGridItem.swift in Sources */, D0EC6DC41EB9F58900EBF1C3 /* MultiplexedSoftwareVideoNode.swift in Sources */, D0EC6FDC1EBA135100EBF1C3 /* resample.c in Sources */, + D0E9BAE81F0574FF00F079A4 /* STPCustomer.m in Sources */, D0C0B59F1EE082F5000F4D2C /* ChatSearchInputPanelNode.swift in Sources */, + D079FCD91F05A5550038FADE /* BotCheckoutPasswordEntryController.swift in Sources */, D0EC6DC51EB9F58900EBF1C3 /* SoftwareVideoSource.swift in Sources */, D0EC6DC61EB9F58900EBF1C3 /* MultiplexedSoftwareVideoSourceManager.swift in Sources */, D0EC6DC71EB9F58900EBF1C3 /* SampleBufferPool.swift in Sources */, @@ -4371,14 +4970,17 @@ D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, D0EC6DD91EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */, + D0E9BA161F05574500F079A4 /* STPCardValidator.m in Sources */, D0EC6DDA1EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DDB1EB9F58900EBF1C3 /* ChatInputPanelNode.swift in Sources */, D01BAA221ECE076100295217 /* CallListCallItem.swift in Sources */, D0EC6EAA1EBA0FBB00EBF1C3 /* BufferPool.cpp in Sources */, + D0E9BA081F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift in Sources */, D0EC6DDC1EB9F58900EBF1C3 /* ChatTextInputPanelNode.swift in Sources */, D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingButton.swift in Sources */, D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */, D0EC6DDE1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, + D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */, D0EC6DDF1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, D0EC6DE01EB9F58900EBF1C3 /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, D0EC6FAE1EBA112600EBF1C3 /* delay_estimator.cc in Sources */, @@ -4400,6 +5002,7 @@ D0EC6DEE1EB9F58900EBF1C3 /* ChatMediaActionSheetController.swift in Sources */, D0EC6DEF1EB9F58900EBF1C3 /* ChatMediaActionSheetRollItem.swift in Sources */, D0EC6DF01EB9F58900EBF1C3 /* ActionSheetRollImageItem.swift in Sources */, + D0E9BA0C1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift in Sources */, D0EC6DF11EB9F58900EBF1C3 /* ShareController.swift in Sources */, D0EC6FC21EBA135100EBF1C3 /* auto_corr_to_refl_coef.c in Sources */, D0EC6DF21EB9F58900EBF1C3 /* ShareControllerNode.swift in Sources */, @@ -4410,11 +5013,13 @@ D0EC6FCA1EBA135100EBF1C3 /* division_operations.c in Sources */, D0EC6DF71EB9F58900EBF1C3 /* PeerMediaCollectionTitleView.swift in Sources */, D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */, + D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */, D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */, D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */, D0EC6DFD1EB9F58900EBF1C3 /* GalleryControllerNode.swift in Sources */, + D0E9BA571F055A0B00F079A4 /* STPFormTextField.m in Sources */, D0EC6DFE1EB9F58900EBF1C3 /* GalleryControllerPresentationState.swift in Sources */, D0EC6FC71EBA135100EBF1C3 /* copy_set_operations.c in Sources */, D0EC6DFF1EB9F58900EBF1C3 /* GalleryItem.swift in Sources */, @@ -4422,12 +5027,12 @@ D0EC6E011EB9F58900EBF1C3 /* GalleryPagerNode.swift in Sources */, D0EC6E021EB9F58900EBF1C3 /* GalleryFooterNode.swift in Sources */, D0EC6E031EB9F58900EBF1C3 /* GalleryFooterContentNode.swift in Sources */, + D0E9BA0A1F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift in Sources */, D0EC6E041EB9F58900EBF1C3 /* SecretMediaPreviewController.swift in Sources */, D0EC6E051EB9F58900EBF1C3 /* SecretMediaPreviewControllerNode.swift in Sources */, D0EC6E061EB9F58900EBF1C3 /* ChatDocumentGalleryItem.swift in Sources */, D0EC6E071EB9F58900EBF1C3 /* ChatHoleGalleryItem.swift in Sources */, D0EC6E081EB9F58900EBF1C3 /* ChatImageGalleryItem.swift in Sources */, - D00ADFDF1EBB944900873D2E /* VideoOverlayMediaItem.swift in Sources */, D0EC6E091EB9F58900EBF1C3 /* ChatVideoGalleryItem.swift in Sources */, D0EC6E0A1EB9F58900EBF1C3 /* ChatVideoGalleryItemScrubberView.swift in Sources */, D0EC6FC31EBA135100EBF1C3 /* auto_correlation.c in Sources */, @@ -4435,16 +5040,19 @@ D0EC6E0C1EB9F58900EBF1C3 /* ChatItemGalleryFooterContentNode.swift in Sources */, D0EC6E0D1EB9F58900EBF1C3 /* ChatItemGalleryItemNode.swift in Sources */, D0EC6FD01EBA135100EBF1C3 /* filter_ar_fast_q12.c in Sources */, + D0E9BABD1F05735F00F079A4 /* STPPaymentConfiguration.m in Sources */, D0EC6FEA1EBA17C300EBF1C3 /* fft4g.c in Sources */, D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */, D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */, D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */, D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */, + D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */, D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */, D0EC6E121EB9F58900EBF1C3 /* InstantPageLayout.swift in Sources */, D0EC6E131EB9F58900EBF1C3 /* InstantPageItem.swift in Sources */, D0EC6E141EB9F58900EBF1C3 /* InstantPageMedia.swift in Sources */, D0EC6E151EB9F58900EBF1C3 /* InstantPageLinkSelectionView.swift in Sources */, + D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */, D0EC6EBA1EBA0FD000EBF1C3 /* AudioUnitIO.cpp in Sources */, D0EC6E161EB9F58900EBF1C3 /* InstantPageLayoutSpacings.swift in Sources */, D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */, @@ -4491,8 +5099,10 @@ D0EC6E361EB9F58900EBF1C3 /* ItemListTextWithLabelItem.swift in Sources */, D0EC6E371EB9F58900EBF1C3 /* ItemListActionItem.swift in Sources */, D0EC6E381EB9F58900EBF1C3 /* ItemListDisclosureItem.swift in Sources */, + D0E9BA651F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift in Sources */, D0EC6E391EB9F58900EBF1C3 /* ItemListCheckboxItem.swift in Sources */, D0EC6E3A1EB9F58900EBF1C3 /* ItemListSwitchItem.swift in Sources */, + D09E63A41F0FAB91003444CD /* EmbedGalleryVideoItem.swift in Sources */, D0EC6E3B1EB9F58900EBF1C3 /* ItemListPeerItem.swift in Sources */, D0EC6E3C1EB9F58900EBF1C3 /* ItemListPeerActionItem.swift in Sources */, D0EC6E3D1EB9F58900EBF1C3 /* ItemListMultilineInputItem.swift in Sources */, @@ -4513,6 +5123,7 @@ D0EC6E4B1EB9F58900EBF1C3 /* ItemListControllerSegmentedTitleView.swift in Sources */, D0EC6E4D1EB9F58900EBF1C3 /* PeerInfoController.swift in Sources */, D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */, + D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */, D0EC6E4F1EB9F58900EBF1C3 /* ChannelVisibilityController.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, @@ -4538,6 +5149,7 @@ D0EC6E5F1EB9F58900EBF1C3 /* RecentSessionsController.swift in Sources */, D0EC6E601EB9F58900EBF1C3 /* BlockedPeersController.swift in Sources */, D0EC6E611EB9F58900EBF1C3 /* SelectivePrivacySettingsController.swift in Sources */, + D0471B4B1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift in Sources */, D0EC6E621EB9F58900EBF1C3 /* SelectivePrivacySettingsPeersController.swift in Sources */, D0EC6EA91EBA0FBB00EBF1C3 /* BufferOutputStream.cpp in Sources */, D0EC6E631EB9F58900EBF1C3 /* TwoStepVerificationUnlockController.swift in Sources */, @@ -4550,6 +5162,7 @@ D0EC6E681EB9F58900EBF1C3 /* VoiceCallDataSavingController.swift in Sources */, D0EC6E691EB9F58900EBF1C3 /* NetworkUsageStatsController.swift in Sources */, D0EC6E6A1EB9F58900EBF1C3 /* StorageUsageController.swift in Sources */, + D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */, D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */, D0EC6EB31EBA0FC100EBF1C3 /* AudioInput.cpp in Sources */, D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */, @@ -4562,6 +5175,7 @@ D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */, D0EC6FD91EBA135100EBF1C3 /* randomization_functions.c in Sources */, D0EC6E721EB9F58900EBF1C3 /* ThemeGalleryItem.swift in Sources */, + D0471B581EFE6D020074D609 /* BotCheckoutInfoController.swift in Sources */, D0EC6E731EB9F58900EBF1C3 /* ThemeGalleryToolbarNode.swift in Sources */, D0EC6FC41EBA135100EBF1C3 /* complex_bit_reverse.c in Sources */, D0EC6E741EB9F58900EBF1C3 /* ThemeGridController.swift in Sources */, @@ -4577,11 +5191,13 @@ D0EC6E7B1EB9F58900EBF1C3 /* DebugAccountsController.swift in Sources */, D0EC6FE71EBA135100EBF1C3 /* vector_scaling_operations.c in Sources */, D0EC6E7C1EB9F58900EBF1C3 /* UsernameSetupController.swift in Sources */, + D0471B621EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift in Sources */, D0EC6E7D1EB9F58900EBF1C3 /* ChangePhoneNumberIntroController.swift in Sources */, D0EC6FA31EBA10E400EBF1C3 /* checks.cc in Sources */, D0EC6E7E1EB9F58900EBF1C3 /* ChangePhoneNumberController.swift in Sources */, D0EC6E7F1EB9F58900EBF1C3 /* ChangePhoneNumberControllerNode.swift in Sources */, D0EC6E801EB9F58900EBF1C3 /* ChangePhoneNumberCodeController.swift in Sources */, + D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */, D0EC6E811EB9F58900EBF1C3 /* NotificationContainerController.swift in Sources */, D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */, D0EC6E821EB9F58900EBF1C3 /* NotificationContainerControllerNode.swift in Sources */, @@ -4596,6 +5212,7 @@ D0EC6E8B1EB9F58900EBF1C3 /* RingBuffer.m in Sources */, D0EC6FBE1EBA132B00EBF1C3 /* ns_core.c in Sources */, D0EC6E8C1EB9F58900EBF1C3 /* RingByteBuffer.swift in Sources */, + D0E9BA181F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m in Sources */, D0EC6E8D1EB9F58900EBF1C3 /* SecretChatKeyVisualization.m in Sources */, D0EC6E8E1EB9F58900EBF1C3 /* NumberPluralizationForm.m in Sources */, ); @@ -4688,7 +5305,7 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - D0400EDB1D5B900A007931CE /* Hockeyapp */ = { + D0400EDB1D5B900A007931CE /* Release AppStore */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -4733,6 +5350,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = ""; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; @@ -4741,9 +5359,9 @@ VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Hockeyapp; + name = "Release AppStore"; }; - D0400EDD1D5B900A007931CE /* Hockeyapp */ = { + D0400EDD1D5B900A007931CE /* Release AppStore */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -4756,113 +5374,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.1; }; - name = Hockeyapp; + name = "Release AppStore"; }; - D0EC6E9E1EB9F79800EBF1C3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = X834Q8SBVP; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - HEADER_SEARCH_PATHS = "third-party/ogg"; - INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/third-party/opus/lib", - "$(PROJECT_DIR)/third-party/libwebp/lib", - "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", - ); - OTHER_CFLAGS = ( - "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", - "-DWEBRTC_APM_DEBUG_DUMP=0", - "-DWEBRTC_POSIX", - ); - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; - PRODUCT_NAME = TelegramUI; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; - USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; - }; - name = Debug; - }; - D0EC6E9F1EB9F79800EBF1C3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - COPY_PHASE_STRIP = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = X834Q8SBVP; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - HEADER_SEARCH_PATHS = "third-party/ogg"; - INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/third-party/opus/lib", - "$(PROJECT_DIR)/third-party/libwebp/lib", - "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", - ); - OTHER_CFLAGS = ( - "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", - "-DWEBRTC_APM_DEBUG_DUMP=0", - "-DWEBRTC_POSIX", - ); - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; - PRODUCT_NAME = TelegramUI; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; - USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; - }; - name = Release; - }; - D0EC6EA01EB9F79800EBF1C3 /* Hockeyapp */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEVELOPMENT_TEAM = X834Q8SBVP; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - HEADER_SEARCH_PATHS = "third-party/ogg"; - INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/third-party/opus/lib", - "$(PROJECT_DIR)/third-party/libwebp/lib", - "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", - ); - OTHER_CFLAGS = ( - "-DTGVOIP_USE_CUSTOM_CRYPTO", - "-DTGVOIP_USE_AUDIO_SESSION", - "-DWEBRTC_APM_DEBUG_DUMP=0", - "-DWEBRTC_POSIX", - ); - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; - PRODUCT_NAME = TelegramUI; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; - USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; - }; - name = Hockeyapp; - }; - D0FC40911D5B8E7500261D9D /* Debug */ = { + D079FD261F06BEF70038FADE /* Debug AppStore */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -4914,6 +5428,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = ""; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -4922,9 +5437,226 @@ VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Debug; + name = "Debug AppStore"; }; - D0FC40921D5B8E7500261D9D /* Release */ = { + D079FD271F06BEF70038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = TelegramUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.1; + }; + name = "Debug AppStore"; + }; + D079FD281F06BEF70038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DTGVOIP_USE_AUDIO_SESSION", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; + }; + name = "Debug AppStore"; + }; + D0EC6E9E1EB9F79800EBF1C3 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DTGVOIP_USE_AUDIO_SESSION", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; + }; + name = "Debug Hockeyapp"; + }; + D0EC6E9F1EB9F79800EBF1C3 /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DTGVOIP_USE_AUDIO_SESSION", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; + }; + name = "Release Hockeyapp"; + }; + D0EC6EA01EB9F79800EBF1C3 /* Release AppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEVELOPMENT_TEAM = X834Q8SBVP; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "third-party/ogg"; + INFOPLIST_FILE = "$(SRCROOT)/TelegramUI/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/third-party/opus/lib", + "$(PROJECT_DIR)/third-party/libwebp/lib", + "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", + ); + OTHER_CFLAGS = ( + "-DTGVOIP_USE_CUSTOM_CRYPTO", + "-DTGVOIP_USE_AUDIO_SESSION", + "-DWEBRTC_APM_DEBUG_DUMP=0", + "-DWEBRTC_POSIX", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; + PRODUCT_NAME = TelegramUI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = submodules/libtgvoip/webrtc_dsp; + }; + name = "Release AppStore"; + }; + D0FC40911D5B8E7500261D9D /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = ""; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = "Debug Hockeyapp"; + }; + D0FC40921D5B8E7500261D9D /* Release Hockeyapp */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -4969,6 +5701,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = ""; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; @@ -4977,9 +5710,9 @@ VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Release; + name = "Release Hockeyapp"; }; - D0FC40971D5B8E7500261D9D /* Debug */ = { + D0FC40971D5B8E7500261D9D /* Debug Hockeyapp */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -4991,9 +5724,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.1; }; - name = Debug; + name = "Debug Hockeyapp"; }; - D0FC40981D5B8E7500261D9D /* Release */ = { + D0FC40981D5B8E7500261D9D /* Release Hockeyapp */ = { isa = XCBuildConfiguration; baseConfigurationReference = D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */; buildSettings = { @@ -5006,7 +5739,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.1; }; - name = Release; + name = "Release Hockeyapp"; }; /* End XCBuildConfiguration section */ @@ -5014,32 +5747,35 @@ D0EC6EA11EB9F79800EBF1C3 /* Build configuration list for PBXNativeTarget "TelegramUI" */ = { isa = XCConfigurationList; buildConfigurations = ( - D0EC6E9E1EB9F79800EBF1C3 /* Debug */, - D0EC6E9F1EB9F79800EBF1C3 /* Release */, - D0EC6EA01EB9F79800EBF1C3 /* Hockeyapp */, + D0EC6E9E1EB9F79800EBF1C3 /* Debug Hockeyapp */, + D079FD281F06BEF70038FADE /* Debug AppStore */, + D0EC6E9F1EB9F79800EBF1C3 /* Release Hockeyapp */, + D0EC6EA01EB9F79800EBF1C3 /* Release AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release Hockeyapp"; }; D0FC40791D5B8E7400261D9D /* Build configuration list for PBXProject "TelegramUI" */ = { isa = XCConfigurationList; buildConfigurations = ( - D0FC40911D5B8E7500261D9D /* Debug */, - D0FC40921D5B8E7500261D9D /* Release */, - D0400EDB1D5B900A007931CE /* Hockeyapp */, + D0FC40911D5B8E7500261D9D /* Debug Hockeyapp */, + D079FD261F06BEF70038FADE /* Debug AppStore */, + D0FC40921D5B8E7500261D9D /* Release Hockeyapp */, + D0400EDB1D5B900A007931CE /* Release AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release Hockeyapp"; }; D0FC40961D5B8E7500261D9D /* Build configuration list for PBXNativeTarget "TelegramUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( - D0FC40971D5B8E7500261D9D /* Debug */, - D0FC40981D5B8E7500261D9D /* Release */, - D0400EDD1D5B900A007931CE /* Hockeyapp */, + D0FC40971D5B8E7500261D9D /* Debug Hockeyapp */, + D079FD271F06BEF70038FADE /* Debug AppStore */, + D0FC40981D5B8E7500261D9D /* Release Hockeyapp */, + D0400EDD1D5B900A007931CE /* Release AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release Hockeyapp"; }; /* End XCConfigurationList section */ }; diff --git a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index 4c50bc14ee..4dab2d7cd2 100644 --- a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TelegramUI.xcscheme orderHint - 12 + 13 SuppressBuildableAutocreation diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 68c7a70fb8..3a134c2cec 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -336,7 +336,7 @@ public func archivedStickerPacksController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 3778808560..665f2b0cb4 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -93,7 +93,7 @@ public final class AuthorizationSequenceController: NavigationController { text = "An error occurred. Please try again later." } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } })) } @@ -135,7 +135,7 @@ public final class AuthorizationSequenceController: NavigationController { text = "An error occured." } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } } })) @@ -178,7 +178,7 @@ public final class AuthorizationSequenceController: NavigationController { text = "An error occured. Please try again later." } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } } })) @@ -225,7 +225,7 @@ public final class AuthorizationSequenceController: NavigationController { text = "An error occured. Please try again later." } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } } })) diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 1682e0fa80..4a92ac56c4 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -2,252 +2,77 @@ import Foundation import Display import AsyncDisplayKit -private let countryNamesAndCodes: [(String, Int)] = [ - ("Jamaica", 1876), - ("Saint Kitts & Nevis", 1869), - ("Trinidad & Tobago", 1868), - ("Saint Vincent & the Grenadines", 1784), - ("Dominica", 1767), - ("Saint Lucia", 1758), - ("Sint Maarten", 1721), - ("American Samoa", 1684), - ("Guam", 1671), - ("Northern Mariana Islands", 1670), - ("Montserrat", 1664), - ("Turks & Caicos Islands", 1649), - ("Grenada", 1473), - ("Bermuda", 1441), - ("Cayman Islands", 1345), - ("US Virgin Islands", 1340), - ("British Virgin Islands", 1284), - ("Antigua & Barbuda", 1268), - ("Anguilla", 1264), - ("Barbados", 1246), - ("Bahamas", 1242), - ("Uzbekistan", 998), - ("Kyrgyzstan", 996), - ("Georgia", 995), - ("Azerbaijan", 994), - ("Turkmenistan", 993), - ("Tajikistan", 992), - ("Nepal", 977), - ("Mongolia", 976), - ("Bhutan", 975), - ("Qatar", 974), - ("Bahrain", 973), - ("Israel", 972), - ("United Arab Emirates", 971), - ("Palestine", 970), - ("Oman", 968), - ("Yemen", 967), - ("Saudi Arabia", 966), - ("Kuwait", 965), - ("Iraq", 964), - ("Syrian Arab Republic", 963), - ("Jordan", 962), - ("Lebanon", 961), - ("Maldives", 960), - ("Taiwan", 886), - ("Bangladesh", 880), - ("Laos", 856), - ("Cambodia", 855), - ("Macau", 853), - ("Hong Kong", 852), - ("North Korea", 850), - ("Marshall Islands", 692), - ("Micronesia", 691), - ("Tokelau", 690), - ("French Polynesia", 689), - ("Tuvalu", 688), - ("New Caledonia", 687), - ("Kiribati", 686), - ("Samoa", 685), - ("Niue", 683), - ("Cook Islands", 682), - ("Wallis & Futuna", 681), - ("Palau", 680), - ("Fiji", 679), - ("Vanuatu", 678), - ("Solomon Islands", 677), - ("Tonga", 676), - ("Papua New Guinea", 675), - ("Nauru", 674), - ("Brunei Darussalam", 673), - ("Norfolk Island", 672), - ("Timor-Leste", 670), - ("Bonaire, Sint Eustatius & Saba", 599), - ("Curaçao", 599), - ("Uruguay", 598), - ("Suriname", 597), - ("Martinique", 596), - ("Paraguay", 595), - ("French Guiana", 594), - ("Ecuador", 593), - ("Guyana", 592), - ("Bolivia", 591), - ("Guadeloupe", 590), - ("Haiti", 509), - ("Saint Pierre & Miquelon", 508), - ("Panama", 507), - ("Costa Rica", 506), - ("Nicaragua", 505), - ("Honduras", 504), - ("El Salvador", 503), - ("Guatemala", 502), - ("Belize", 501), - ("Falkland Islands", 500), - ("Liechtenstein", 423), - ("Slovakia", 421), - ("Czech Republic", 420), - ("Macedonia", 389), - ("Bosnia & Herzegovina", 387), - ("Slovenia", 386), - ("Croatia", 385), - ("Montenegro", 382), - ("Serbia", 381), - ("Ukraine", 380), - ("San Marino", 378), - ("Monaco", 377), - ("Andorra", 376), - ("Belarus", 375), - ("Armenia", 374), - ("Moldova", 373), - ("Estonia", 372), - ("Latvia", 371), - ("Lithuania", 370), - ("Bulgaria", 359), - ("Finland", 358), - ("Cyprus", 357), - ("Malta", 356), - ("Albania", 355), - ("Iceland", 354), - ("Ireland", 353), - ("Luxembourg", 352), - ("Portugal", 351), - ("Gibraltar", 350), - ("Greenland", 299), - ("Faroe Islands", 298), - ("Aruba", 297), - ("Eritrea", 291), - ("Saint Helena", 290), - ("Comoros", 269), - ("Swaziland", 268), - ("Botswana", 267), - ("Lesotho", 266), - ("Malawi", 265), - ("Namibia", 264), - ("Zimbabwe", 263), - ("Réunion", 262), - ("Madagascar", 261), - ("Zambia", 260), - ("Mozambique", 258), - ("Burundi", 257), - ("Uganda", 256), - ("Tanzania", 255), - ("Kenya", 254), - ("Djibouti", 253), - ("Somalia", 252), - ("Ethiopia", 251), - ("Rwanda", 250), - ("Sudan", 249), - ("Seychelles", 248), - ("Saint Helena", 247), - ("Diego Garcia", 246), - ("Guinea-Bissau", 245), - ("Angola", 244), - ("Congo (Dem. Rep.)", 243), - ("Congo (Rep.)", 242), - ("Gabon", 241), - ("Equatorial Guinea", 240), - ("São Tomé & Príncipe", 239), - ("Cape Verde", 238), - ("Cameroon", 237), - ("Central African Rep.", 236), - ("Chad", 235), - ("Nigeria", 234), - ("Ghana", 233), - ("Sierra Leone", 232), - ("Liberia", 231), - ("Mauritius", 230), - ("Benin", 229), - ("Togo", 228), - ("Niger", 227), - ("Burkina Faso", 226), - ("Côte d`Ivoire", 225), - ("Guinea", 224), - ("Mali", 223), - ("Mauritania", 222), - ("Senegal", 221), - ("Gambia", 220), - ("Libya", 218), - ("Tunisia", 216), - ("Algeria", 213), - ("Morocco", 212), - ("South Sudan", 211), - ("Iran", 98), - ("Myanmar", 95), - ("Sri Lanka", 94), - ("Afghanistan", 93), - ("Pakistan", 92), - ("India", 91), - ("Turkey", 90), - ("China", 86), - ("Vietnam", 84), - ("South Korea", 82), - ("Japan", 81), - ("Thailand", 66), - ("Singapore", 65), - ("New Zealand", 64), - ("Philippines", 63), - ("Indonesia", 62), - ("Australia", 61), - ("Malaysia", 60), - ("Venezuela", 58), - ("Colombia", 57), - ("Chile", 56), - ("Brazil", 55), - ("Argentina", 54), - ("Cuba", 53), - ("Mexico", 52), - ("Peru", 51), - ("Germany", 49), - ("Poland", 48), - ("Norway", 47), - ("Sweden", 46), - ("Denmark", 45), - ("United Kingdom", 44), - ("Austria", 43), - ("Switzerland", 41), - ("Romania", 40), - ("Italy", 39), - ("Hungary", 36), - ("Spain", 34), - ("France", 33), - ("Belgium", 32), - ("Netherlands", 31), - ("Greece", 30), - ("South Africa", 27), - ("Egypt", 20), - ("Russian Federation", 7), - ("Kazakhstan", 7), - ("USA", 1), - ("Puerto Rico", 1), - ("Dominican Rep.", 1), - ("Canada", 1) -] +private func loadCountryNamesAndCodes() -> [(String, String, Int)] { + guard let filePath = frameworkBundle.path(forResource: "PhoneCountries", ofType: "txt") else { + return [] + } + guard let stringData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [] + } + guard let data = String(data: stringData, encoding: .utf8) else { + return [] + } + + let delimiter = ";" + let endOfLine = "\n" + + var result: [(String, String, Int)] = [] + + var currentLocation = data.startIndex + + while true { + guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex) else { + break + } + + let countryCode = data.substring(with: currentLocation ..< codeRange.lowerBound) + + guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else { + break + } + + let countryId = data.substring(with: codeRange.upperBound ..< idRange.lowerBound) + + let maybeNameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) + let nameRangeIndex = maybeNameRange?.lowerBound ?? data.endIndex + + var countryName = data.substring(with: idRange.upperBound ..< nameRangeIndex) + if countryName.hasSuffix("\r") { + countryName = countryName.substring(to: countryName.index(before: countryName.endIndex)) + } + + if let countryCodeInt = Int(countryCode) { + result.append((countryName, countryId, countryCodeInt)) + } + + if let maybeNameRange = maybeNameRange { + currentLocation = maybeNameRange.upperBound + } else { + break + } + } + + return result +} + +private let countryNamesAndCodes: [(String, String, Int)] = loadCountryNamesAndCodes() private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { + private let displayCodes: Bool + private let tableView: UITableView - var searchResults: [(String, Int)] = [] { + var searchResults: [(String, String, Int)] = [] { didSet { self.tableView.reloadData() } } - var itemSelected: (((String, Int)) -> Void)? + var itemSelected: (((String, String, Int)) -> Void)? - init() { + init(displayCodes: Bool) { + self.displayCodes = displayCodes + self.tableView = UITableView(frame: CGRect(), style: .plain) super.init(nibName: nil, bundle: nil) @@ -284,7 +109,7 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl cell.accessoryView = label } cell.textLabel?.text = self.searchResults[indexPath.row].0 - if let label = cell.accessoryView as? UILabel { + if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = "+\(self.searchResults[indexPath.row].1)" label.sizeToFit() } @@ -297,29 +122,33 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl } private final class InnerCountrySelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate { + private let displayCodes: Bool + private let tableView: UITableView - private let sections: [(String, [(String, Int)])] + private let sections: [(String, [(String, String, Int)])] private let sectionTitles: [String] private var searchController: UISearchController! private var searchResultsController: InnerCoutrySearchResultsController! var dismiss: (() -> Void)? - var itemSelected: (((String, Int)) -> Void)? + var itemSelected: (((String, String, Int)) -> Void)? - init() { + init(displayCodes: Bool) { + self.displayCodes = displayCodes + self.tableView = UITableView(frame: CGRect(), style: .plain) - var sections: [(String, [(String, Int)])] = [] - for (name, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in + var sections: [(String, [(String, String, Int)])] = [] + for (name, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in return lhs.0 < rhs.0 }) { let title = name.substring(to: name.index(after: name.startIndex)).uppercased() if sections.isEmpty || sections[sections.count - 1].0 != title { sections.append((title, [])) } - sections[sections.count - 1].1.append((name, code)) + sections[sections.count - 1].1.append((name, id, code)) } self.sections = sections var sectionTitles = sections.map { $0.0 } @@ -343,7 +172,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi self.view.backgroundColor = .white - self.searchResultsController = InnerCoutrySearchResultsController() + self.searchResultsController = InnerCoutrySearchResultsController(displayCodes: self.displayCodes) self.searchResultsController.itemSelected = { [weak self] item in self?.itemSelected?(item) } @@ -397,7 +226,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi cell.accessoryView = label } cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0 - if let label = cell.accessoryView as? UILabel { + if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = "+\(self.sections[indexPath.section].1[indexPath.row].1)" label.sizeToFit() } @@ -414,7 +243,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi return } - var results: [(String, Int)] = [] + var results: [(String, String, Int)] = [] for (_, items) in self.sections { for item in items { if item.0.lowercased().hasPrefix(normalizedQuery) { @@ -431,6 +260,15 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi } final class AuthorizationSequenceCountrySelectionController: ViewController { + static func lookupCountryNameById(_ id: String) -> String? { + for (name, itemId, _) in countryNamesAndCodes { + if id == itemId { + return name + } + } + return nil + } + private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode { return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode } @@ -438,10 +276,10 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { private let innerNavigationController: UINavigationController private let innerController: InnerCountrySelectionController - var completeWithCountryCode: ((Int) -> Void)? + var completeWithCountryCode: ((Int, String) -> Void)? - init() { - self.innerController = InnerCountrySelectionController() + init(displayCodes: Bool = true) { + self.innerController = InnerCountrySelectionController(displayCodes: displayCodes) self.innerNavigationController = UINavigationController(rootViewController: self.innerController) super.init(navigationBarTheme: nil) @@ -464,8 +302,8 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { self.displayNode.view.addSubview(self.innerNavigationController.view) self.innerNavigationController.didMove(toParentViewController: self) - self.innerController.itemSelected = { [weak self] _, code in - self?.completeWithCountryCode?(code) + self.innerController.itemSelected = { [weak self] _, countryId, code in + self?.completeWithCountryCode?(code, countryId) self?.controllerNode.animateOut() } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index e328a696f8..d5572e600b 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -49,14 +49,14 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { let controller = AuthorizationSequenceCountrySelectionController() - controller.completeWithCountryCode = { code in + controller.completeWithCountryCode = { code, _ in if let strongSelf = self, let currentData = strongSelf.currentData { strongSelf.updateData(countryCode: Int32(code), number: currentData.1) strongSelf.controllerNode.activateInput() } } strongSelf.controllerNode.view.endEditing(true) - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } } } diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 366b4652f9..f7404dc443 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -189,14 +189,14 @@ class AvatarGalleryController: ViewController { override func loadDisplayNode() { let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in if let strongSelf = self { - strongSelf.present(controller, in: .window, with: arguments) + strongSelf.present(controller, in: .window(.root), with: arguments) + } + }, dismissController: { [weak self] in + self?.dismiss(forceAway: true) + }, replaceRootController: { [weak self] controller, ready in + if let strongSelf = self { + strongSelf.replaceRootController(controller, ready) } - }, dismissController: { [weak self] in - self?.dismiss(forceAway: true) - }, replaceRootController: { [weak self] controller, ready in - if let strongSelf = self { - strongSelf.replaceRootController(controller, ready) - } }) self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction) self.displayNodeDidLoad() diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index 949995c4d0..2c7da6102b 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -278,7 +278,7 @@ public func blockedPeersController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/BotCheckoutActionButton.swift b/TelegramUI/BotCheckoutActionButton.swift new file mode 100644 index 0000000000..747a768f38 --- /dev/null +++ b/TelegramUI/BotCheckoutActionButton.swift @@ -0,0 +1,250 @@ +import Foundation +import AsyncDisplayKit +import Display +import PassKit + +enum BotCheckoutActionButtonState: Equatable { + case loading + case active(String) + case inactive(String) + case applePay + + static func ==(lhs: BotCheckoutActionButtonState, rhs: BotCheckoutActionButtonState) -> Bool { + switch lhs { + case .loading: + if case .loading = rhs { + return true + } else { + return false + } + case let .active(title): + if case .active(title) = rhs { + return true + } else { + return false + } + case let .inactive(title): + if case .inactive(title) = rhs { + return true + } else { + return false + } + case .applePay: + if case .applePay = rhs { + return true + } else { + return false + } + } + } +} + +private let titleFont = Font.semibold(17.0) + +final class BotCheckoutActionButton: HighlightTrackingButtonNode { + static var diameter: CGFloat = 48.0 + + private var inactiveFillColor: UIColor + private var activeFillColor: UIColor + private var foregroundColor: UIColor + + private let progressBackgroundNode: ASImageNode + private let inactiveBackgroundNode: ASImageNode + private let activeBackgroundNode: ASImageNode + private var applePayButton: UIButton? + private let labelNode: TextNode + + private var state: BotCheckoutActionButtonState? + private var validLayout: CGSize? + + init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) { + self.inactiveFillColor = inactiveFillColor + self.activeFillColor = activeFillColor + self.foregroundColor = foregroundColor + + self.progressBackgroundNode = ASImageNode() + self.progressBackgroundNode.displaysAsynchronously = false + self.progressBackgroundNode.displayWithoutProcessing = true + self.progressBackgroundNode.isLayerBacked = true + self.progressBackgroundNode.image = generateImage(CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + let strokeWidth: CGFloat = 2.0 + context.setFillColor(activeFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(inactiveFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0))) + let cutout: CGFloat = 10.0 + context.fill(CGRect(origin: CGPoint(x: floor((size.width - cutout) / 2.0), y: 0.0), size: CGSize(width: cutout, height: cutout))) + }) + + self.inactiveBackgroundNode = ASImageNode() + self.inactiveBackgroundNode.displaysAsynchronously = false + self.inactiveBackgroundNode.displayWithoutProcessing = true + self.inactiveBackgroundNode.isLayerBacked = true + self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0) + self.inactiveBackgroundNode.alpha = 0.0 + + self.activeBackgroundNode = ASImageNode() + self.activeBackgroundNode.displaysAsynchronously = false + self.activeBackgroundNode.displayWithoutProcessing = true + self.activeBackgroundNode.isLayerBacked = true + self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: activeFillColor) + + self.labelNode = TextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.progressBackgroundNode) + self.addSubnode(self.inactiveBackgroundNode) + self.addSubnode(self.activeBackgroundNode) + self.addSubnode(self.labelNode) + } + + func setState(_ state: BotCheckoutActionButtonState) { + if self.state != state { + let previousState = self.state + self.state = state + + if let validLayout = self.validLayout, let previousState = previousState { + switch state { + case .loading: + self.inactiveBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.inactiveBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.inactiveBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.activeBackgroundNode.alpha = 0.0 + self.activeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = 0.8 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + + self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation") + case let .active(title): + if case .active = previousState { + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let _ = labelApply() + } else { + self.inactiveBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.inactiveBackgroundNode.alpha = 1.0 + self.progressBackgroundNode.alpha = 0.0 + + self.activeBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.activeBackgroundNode.alpha = 1.0 + self.activeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let _ = labelApply() + self.labelNode.alpha = 1.0 + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + case let .inactive(title): + if case .inactive = previousState { + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let _ = labelApply() + } else { + self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.inactiveBackgroundNode.alpha = 1.0 + self.progressBackgroundNode.alpha = 0.0 + + self.activeBackgroundNode.alpha = 0.0 + + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, validLayout, .natural, nil, UIEdgeInsets()) + self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + let _ = labelApply() + self.labelNode.alpha = 1.0 + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + case .applePay: + if case .applePay = previousState { + + } else { + + } + } + } else { + switch state { + case .loading: + self.labelNode.alpha = 0.0 + self.progressBackgroundNode.alpha = 1.0 + self.activeBackgroundNode.alpha = 0.0 + + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = 0.8 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + + self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation") + case .active: + self.labelNode.alpha = 1.0 + self.progressBackgroundNode.alpha = 0.0 + self.inactiveBackgroundNode.alpha = 0.0 + self.activeBackgroundNode.alpha = 1.0 + case .inactive: + self.labelNode.alpha = 1.0 + self.progressBackgroundNode.alpha = 0.0 + self.inactiveBackgroundNode.alpha = 1.0 + self.activeBackgroundNode.alpha = 0.0 + case .applePay: + self.labelNode.alpha = 0.0 + self.progressBackgroundNode.alpha = 0.0 + self.inactiveBackgroundNode.alpha = 0.0 + self.activeBackgroundNode.alpha = 0.0 + if self.applePayButton == nil { + if #available(iOSApplicationExtension 8.3, *) { + let applePayButton = PKPaymentButton(type: .buy, style: .black) + self.view.addSubview(applePayButton) + self.applePayButton = applePayButton + } + } + } + } + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.diameter) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter))) + transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))) + transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))) + if let applePayButton = self.applePayButton { + applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)) + } + + var labelSize = self.labelNode.bounds.size + if let state = self.state { + switch state { + case let .active(title): + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), nil, 1, .end, size, .natural, nil, UIEdgeInsets()) + let _ = labelApply() + labelSize = labelLayout.size + case let .inactive(title): + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), nil, 1, .end, size, .natural, nil, UIEdgeInsets()) + let _ = labelApply() + labelSize = labelLayout.size + default: + break + } + } + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: floor((size.height - labelSize.height) / 2.0)), size: labelSize)) + } +} diff --git a/TelegramUI/BotCheckoutController.swift b/TelegramUI/BotCheckoutController.swift index 9a30829567..7da0438ef7 100644 --- a/TelegramUI/BotCheckoutController.swift +++ b/TelegramUI/BotCheckoutController.swift @@ -1,3 +1,94 @@ import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox - +final class BotCheckoutController: ViewController { + private var controllerNode: BotCheckoutControllerNode { + return self.displayNode as! BotCheckoutControllerNode + } + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + private let account: Account + private let invoice: TelegramMediaInvoice + private let messageId: MessageId + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + init(account: Account, invoice: TelegramMediaInvoice, messageId: MessageId) { + self.account = account + self.invoice = invoice + self.messageId = messageId + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + var title = self.presentationData.strings.Checkout_Title + if invoice.flags.contains(.isTest) { + title += " (Test)" + } + self.title = title + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + let displayNode = BotCheckoutControllerNode(updateNavigationOffset: { [weak self] offset in + if let strongSelf = self { + strongSelf.navigationOffset = offset + } + }, account: self.account, invoice: self.invoice, messageId: self.messageId, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, dismissAnimated: { [weak self] in + self?.dismiss() + }) + + //displayNode.enableInteractiveDismiss = true + + displayNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.displayNode = displayNode + super.displayNodeDidLoad() + self._ready.set(displayNode.ready) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + @objc func cancelPressed() { + self.dismiss() + } +} diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift new file mode 100644 index 0000000000..0f26802eaa --- /dev/null +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -0,0 +1,836 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import PassKit + +import TelegramUIPrivateModule + +final class BotCheckoutControllerArguments { + fileprivate let account: Account + fileprivate let openInfo: (BotCheckoutInfoControllerFocus) -> Void + fileprivate let openPaymentMethod: () -> Void + fileprivate let openShippingMethod: () -> Void + + fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void) { + self.account = account + self.openInfo = openInfo + self.openPaymentMethod = openPaymentMethod + self.openShippingMethod = openShippingMethod + } +} + +private enum BotCheckoutSection: Int32 { + case header + case prices + case info +} + +enum BotCheckoutEntry: ItemListNodeEntry { + case header(PresentationTheme, TelegramMediaInvoice, String) + case price(Int, PresentationTheme, String, String, Bool) + case paymentMethod(PresentationTheme, String, String) + case shippingInfo(PresentationTheme, String, String) + case shippingMethod(PresentationTheme, String, String) + case nameInfo(PresentationTheme, String, String) + case emailInfo(PresentationTheme, String, String) + case phoneInfo(PresentationTheme, String, String) + + var section: ItemListSectionId { + switch self { + case .header: + return BotCheckoutSection.header.rawValue + case .price: + return BotCheckoutSection.prices.rawValue + default: + return BotCheckoutSection.info.rawValue + } + } + + var stableId: Int32 { + switch self { + case .header: + return 0 + case let .price(index, _, _, _, _): + return 1 + Int32(index) + case .paymentMethod: + return 10000 + 0 + case .shippingInfo: + return 10000 + 1 + case .shippingMethod: + return 10000 + 2 + case .nameInfo: + return 10000 + 3 + case .emailInfo: + return 10000 + 4 + case .phoneInfo: + return 10000 + 5 + } + } + + static func ==(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsInvoice, lhsName): + if case let .header(rhsTheme, rhsInvoice, rhsName) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if !lhsInvoice.isEqual(rhsInvoice) { + return false + } + if lhsName != rhsName { + return false + } + return true + } else { + return false + } + case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal): + if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsText != rhsText { + return false + } + if lhsValue != rhsValue { + return false + } + if lhsFinal != rhsFinal { + return false + } + return true + } else { + return false + } + case let .paymentMethod(lhsTheme, lhsText, lhsValue): + if case let .paymentMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shippingInfo(lhsTheme, lhsText, lhsValue): + if case let .shippingInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shippingMethod(lhsTheme, lhsText, lhsValue): + if case let .shippingMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .nameInfo(lhsTheme, lhsText, lhsValue): + if case let .nameInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .emailInfo(lhsTheme, lhsText, lhsValue): + if case let .emailInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .phoneInfo(lhsTheme, lhsText, lhsValue): + if case let .phoneInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + } + } + + static func <(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: BotCheckoutControllerArguments) -> ListViewItem { + switch self { + case let .header(theme, invoice, botName): + return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) + case let .price(_, theme, text, value, isFinal): + return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section) + case let .paymentMethod(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openPaymentMethod() + }) + case let .shippingInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openInfo(.address) + }) + case let .shippingMethod(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openShippingMethod() + }) + case let .nameInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openInfo(.name) + }) + case let .emailInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openInfo(.email) + }) + case let .phoneInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openInfo(.phone) + }) + } + } +} + +private struct BotCheckoutControllerState: Equatable { + init() { + } + + static func ==(lhs: BotCheckoutControllerState, rhs: BotCheckoutControllerState) -> Bool { + return true + } +} + +private func botCheckoutControllerEntries(presentationData: PresentationData, state: BotCheckoutControllerState, invoice: TelegramMediaInvoice, paymentForm: BotPaymentForm?, formInfo: BotPaymentRequestedInfo?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentPaymentMethod: BotCheckoutPaymentMethod?, botPeer: Peer?) -> [BotCheckoutEntry] { + var entries: [BotCheckoutEntry] = [] + + var botName = "" + if let botPeer = botPeer { + botName = botPeer.displayTitle + } + entries.append(.header(presentationData.theme, invoice, botName)) + + if let paymentForm = paymentForm { + var totalPrice: Int64 = 0 + + var index = 0 + for price in paymentForm.invoice.prices { + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false)) + totalPrice += price.amount + index += 1 + } + + var shippingOptionString: String? + if let validatedFormInfo = validatedFormInfo, let shippingOptions = validatedFormInfo.shippingOptions { + shippingOptionString = "" + if let currentShippingOptionId = currentShippingOptionId { + for option in shippingOptions { + if option.id == currentShippingOptionId { + shippingOptionString = option.title + + for price in option.prices { + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false)) + totalPrice += price.amount + index += 1 + } + + break + } + } + } + } + + entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true)) + + var paymentMethodTitle = "" + if let currentPaymentMethod = currentPaymentMethod { + paymentMethodTitle = currentPaymentMethod.title + } + entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle)) + if paymentForm.invoice.requestedFields.contains(.shippingAddress) { + var addressString = "" + if let address = formInfo?.shippingAddress { + let components: [String] = [ + address.city, + address.streetLine1, + address.streetLine2, + address.state + ] + for component in components { + if !component.isEmpty { + if !addressString.isEmpty { + addressString.append(", ") + } + addressString.append(component) + } + } + } + entries.append(.shippingInfo(presentationData.theme, presentationData.strings.Checkout_ShippingAddress, addressString)) + + if let shippingOptionString = shippingOptionString { + entries.append(.shippingMethod(presentationData.theme, presentationData.strings.Checkout_ShippingMethod, shippingOptionString)) + } + } + + if paymentForm.invoice.requestedFields.contains(.name) { + entries.append(.nameInfo(presentationData.theme, presentationData.strings.Checkout_Name, formInfo?.name ?? "")) + } + + if paymentForm.invoice.requestedFields.contains(.email) { + entries.append(.emailInfo(presentationData.theme, presentationData.strings.Checkout_Email, formInfo?.email ?? "")) + } + + if paymentForm.invoice.requestedFields.contains(.phone) { + entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? "")) + } + } + + return entries +} + +private let hasApplePaySupport: Bool = PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: [.visa, .masterCard, .amex]) + +private func availablePaymentMethods(current: BotCheckoutPaymentMethod?, supportsApplePay: Bool) -> [BotCheckoutPaymentMethod] { + var methods: [BotCheckoutPaymentMethod] = [] + if hasApplePaySupport { + methods.append(.applePayStripe) + } + if let current = current { + if !methods.contains(current) { + methods.append(current) + } + } + return methods +} + +final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate { + private let account: Account + private let messageId: MessageId + private let present: (ViewController, Any?) -> Void + private let dismissAnimated: () -> Void + + private var stateValue = BotCheckoutControllerState() + private let state = ValuePromise(BotCheckoutControllerState(), ignoreRepeated: true) + private var arguments: BotCheckoutControllerArguments? + + private var presentationData: PresentationData + + private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?)?>(nil) + private var paymentFormValue: BotPaymentForm? + private var currentFormInfo: BotPaymentRequestedInfo? + private var currentValidatedFormInfo: BotPaymentValidatedFormInfo? + private var currentShippingOptionId: String? + private var currentPaymentMethod: BotCheckoutPaymentMethod? + private var formRequestDisposable: Disposable? + + private let actionButton: BotCheckoutActionButton + private let inProgressDimNode: ASDisplayNode + + private let payDisposable = MetaDisposable() + private let paymentAuthDisposable = MetaDisposable() + private var applePayAuthrorizationCompletion: ((PKPaymentAuthorizationStatus) -> Void)? + private var applePayController: PKPaymentAuthorizationViewController? + + init(updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) { + self.account = account + self.messageId = messageId + self.present = present + self.dismissAnimated = dismissAnimated + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + var openInfoImpl: ((BotCheckoutInfoControllerFocus) -> Void)? + var openPaymentMethodImpl: (() -> Void)? + var openShippingMethodImpl: (() -> Void)? + + let arguments = BotCheckoutControllerArguments(account: account, openInfo: { item in + openInfoImpl?(item) + }, openPaymentMethod: { + openPaymentMethodImpl?() + }, openShippingMethod: { + openShippingMethodImpl?() + }) + + let signal: Signal<(PresentationTheme, (ItemListNodeState, BotCheckoutEntry.ItemGenerationArguments)), NoError> = combineLatest(account.telegramApplicationContext.presentationData, self.state.get(), paymentFormAndInfo.get(), account.postbox.loadedPeerWithId(messageId.peerId)) + |> map { presentationData, state, paymentFormAndInfo, botPeer -> (PresentationTheme, (ItemListNodeState, BotCheckoutEntry.ItemGenerationArguments)) in + let nodeState = ItemListNodeState(entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) + + return (presentationData.theme, (nodeState, arguments)) + } + + self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) + self.actionButton.setState(.loading) + + self.inProgressDimNode = ASDisplayNode() + self.inProgressDimNode.alpha = 0.0 + self.inProgressDimNode.isUserInteractionEnabled = false + self.inProgressDimNode.backgroundColor = UIColor.white.withAlphaComponent(0.5) + + super.init(updateNavigationOffset: updateNavigationOffset, state: signal) + + self.arguments = arguments + + openInfoImpl = { [weak self] focus in + if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { + strongSelf.present(BotCheckoutInfoController(account: account, invoice: paymentFormValue.invoice, messageId: messageId, initialFormInfo: currentFormInfo, focus: focus, formInfoUpdated: { formInfo, validatedInfo in + if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue { + strongSelf.currentFormInfo = formInfo + strongSelf.currentValidatedFormInfo = validatedInfo + var updatedCurrentShippingOptionId: String? + if let currentShippingOptionId = strongSelf.currentShippingOptionId, let shippingOptions = validatedInfo.shippingOptions { + if shippingOptions.contains(where: { $0.id == currentShippingOptionId }) { + updatedCurrentShippingOptionId = currentShippingOptionId + } + } + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, formInfo, validatedInfo, updatedCurrentShippingOptionId, strongSelf.currentPaymentMethod))) + } + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + + let applyPaymentMethod: (BotCheckoutPaymentMethod) -> Void = { [weak self] method in + if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { + strongSelf.currentPaymentMethod = method + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod))) + } + } + + let openNewCard: () -> Void = { [weak self] in + if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { + if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" { + guard let paramsData = nativeProvider.params.data(using: .utf8) else { + return + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { + return + } + guard let publishableKey = nativeParams["publishable_key"] as? String else { + return + } + + var additionalFields: BotCheckoutNativeCardEntryAdditionalFields = [] + if let needCardholderName = nativeParams["need_cardholder_name"] as? NSNumber, needCardholderName.boolValue { + additionalFields.insert(.cardholderName) + } + if let needCountry = nativeParams["need_country"] as? NSNumber, needCountry.boolValue { + additionalFields.insert(.country) + } + if let needZip = nativeParams["need_zip"] as? NSNumber, needZip.boolValue { + additionalFields.insert(.zipCode) + } + + var dismissImpl: (() -> Void)? + + let controller = BotCheckoutNativeCardEntryController(account: strongSelf.account, additionalFields: additionalFields, publishableKey: publishableKey, completion: { method in + applyPaymentMethod(method) + dismissImpl?() + }) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + var dismissImpl: (() -> Void)? + let controller = BotCheckoutWebInteractionController(account: account, url: paymentForm.url, intent: .addPaymentMethod({ method in + applyPaymentMethod(method) + dismissImpl?() + })) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + } + + openPaymentMethodImpl = { [weak self] in + if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { + let supportsApplePay: Bool + if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" { + supportsApplePay = true + } else { + supportsApplePay = false + } + let methods = availablePaymentMethods(current: strongSelf.currentPaymentMethod, supportsApplePay: supportsApplePay) + if methods.isEmpty { + openNewCard() + } else { + strongSelf.present(BotCheckoutPaymentMethodSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in + applyPaymentMethod(method) + }, newCard: { + openNewCard() + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + } + + openShippingMethodImpl = { [weak self] in + if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty { + strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in + if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { + strongSelf.currentShippingOptionId = id + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod))) + } + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + + let formAndMaybeValidatedInfo = fetchBotPaymentForm(postbox: account.postbox, network: account.network, messageId: messageId) + |> mapToSignal { paymentForm -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in + if let current = paymentForm.savedInfo { + return validateBotPaymentForm(network: account.network, saveInfo: true, messageId: messageId, formInfo: current) + |> mapError { _ -> BotPaymentFormRequestError in + return .generic + } + |> map { result -> (BotPaymentForm, BotPaymentValidatedFormInfo?) in + return (paymentForm, result) + } + |> `catch` { _ -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in + return .single((paymentForm, nil)) + } + } else { + return .single((paymentForm, nil)) + } + } + + self.formRequestDisposable = (formAndMaybeValidatedInfo |> deliverOnMainQueue).start(next: { [weak self] form, validatedInfo in + if let strongSelf = self { + let savedInfo: BotPaymentRequestedInfo + if let current = form.savedInfo { + savedInfo = current + } else { + savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil) + } + strongSelf.paymentFormValue = form + strongSelf.currentFormInfo = savedInfo + strongSelf.currentValidatedFormInfo = validatedInfo + if let savedCredentials = form.savedCredentials { + strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials) + } + strongSelf.actionButton.isEnabled = true + strongSelf.paymentFormAndInfo.set(.single((form, savedInfo, validatedInfo, nil, strongSelf.currentPaymentMethod))) + strongSelf.actionButton.setState(.active(strongSelf.presentationData.strings.CheckoutInfo_Pay)) + } + }, error: { _ in + + }) + + self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) + self.actionButton.isEnabled = false + self.addSubnode(self.actionButton) + + self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode) + } + + deinit { + self.formRequestDisposable?.dispose() + self.payDisposable.dispose() + self.paymentAuthDisposable.dispose() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var updatedInsets = layout.intrinsicInsets + updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), navigationBarHeight: navigationBarHeight, transition: transition) + + let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) + transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) + self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) + + transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame) + } + + @objc func actionButtonPressed() { + self.pay() + } + + private func pay(savedCredentialsToken: TemporaryTwoStepPasswordToken? = nil, liabilityNoticeAccepted: Bool = false, receivedCredentials: BotPaymentCredentials? = nil) { + guard let paymentForm = self.paymentFormValue else { + return + } + + if !paymentForm.invoice.requestedFields.isEmpty { + guard let validatedFormInfo = self.currentValidatedFormInfo else { + if paymentForm.invoice.requestedFields.contains(.shippingAddress) { + self.arguments?.openInfo(.address) + } else if paymentForm.invoice.requestedFields.contains(.name) { + self.arguments?.openInfo(.name) + } else if paymentForm.invoice.requestedFields.contains(.email) { + self.arguments?.openInfo(.email) + } else if paymentForm.invoice.requestedFields.contains(.phone) { + self.arguments?.openInfo(.phone) + } + return + } + + if let _ = validatedFormInfo.shippingOptions { + if self.currentShippingOptionId == nil { + self.arguments?.openShippingMethod() + return + } + } + } + + guard let paymentMethod = self.currentPaymentMethod else { + self.arguments?.openPaymentMethod() + return + } + + let credentials: BotPaymentCredentials + if let receivedCredentials = receivedCredentials { + credentials = receivedCredentials + } else { + switch paymentMethod { + case let .savedCredentials(savedCredentials): + switch savedCredentials { + case let .card(id, title): + if let savedCredentialsToken = savedCredentialsToken { + credentials = .saved(id: id, tempPassword: savedCredentialsToken.token) + } else { + let _ = (cachedTwoStepPasswordToken(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self { + let timestamp = strongSelf.account.network.getApproximateRemoteTimestamp() + if let token = token, token.validUntilDate > timestamp - 1 * 60 { + if token.requiresBiometrics { + let _ = (LocalAuth.auth(reason: strongSelf.presentationData.strings.Checkout_PayWithTouchId) |> deliverOnMainQueue).start(next: { value in + if let strongSelf = self { + if value { + strongSelf.pay(savedCredentialsToken: token) + } else { + strongSelf.requestPassword(cardTitle: title) + } + } + }) + } else { + strongSelf.pay(savedCredentialsToken: token) + } + } else { + strongSelf.requestPassword(cardTitle: title) + } + } + }) + return + } + } + case let .webToken(_, data, saveOnServer): + credentials = .generic(data: data, saveOnServer: saveOnServer) + case .applePayStripe: + let botPeerId = self.messageId.peerId + let _ = (self.account.postbox.modify({ modifier -> Peer? in + return modifier.getPeer(botPeerId) + }) |> deliverOnMainQueue).start(next: { [weak self] botPeer in + if let strongSelf = self, let botPeer = botPeer { + let request = PKPaymentRequest() + + request.merchantIdentifier = "merchant.ph.telegra.Telegraph" + request.supportedNetworks = [.visa, .amex, .masterCard] + request.merchantCapabilities = [.capability3DS] + request.countryCode = "US" + request.currencyCode = paymentForm.invoice.currency.uppercased() + + var items: [PKPaymentSummaryItem] = [] + + var totalAmount: Int64 = 0 + for price in paymentForm.invoice.prices { + totalAmount += price.amount + + let amount = NSDecimalNumber(value: Double(price.amount) * 0.01) + items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) + } + + if let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, let shippingOptionId = strongSelf.currentShippingOptionId { + if let shippingOptionIndex = shippingOptions.index(where: { $0.id == shippingOptionId }) { + for price in shippingOptions[shippingOptionIndex].prices { + totalAmount += price.amount + + let amount = NSDecimalNumber(value: Double(price.amount) * 0.01) + items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) + } + } + } + + let amount = NSDecimalNumber(value: Double(totalAmount) * 0.01) + items.append(PKPaymentSummaryItem(label: botPeer.displayTitle, amount: amount)) + + request.paymentSummaryItems = items + + let controller = PKPaymentAuthorizationViewController(paymentRequest: request) + controller.delegate = strongSelf + if let window = strongSelf.view.window { + strongSelf.applePayController = controller + window.rootViewController?.present(controller, animated: true) + } + } + }) + return + } + } + + if !liabilityNoticeAccepted { + let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(postbox: self.account.postbox, peerId: self.messageId.peerId), self.account.postbox.loadedPeerWithId(self.messageId.peerId), self.account.postbox.loadedPeerWithId(PeerId(namespace: Namespaces.Peer.CloudUser, id: paymentForm.providerId))) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in + if let strongSelf = self { + if value { + strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) + } else { + strongSelf.present(standardTextAlertController(title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle, providerPeer.displayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + if let strongSelf = self { + let _ = ApplicationSpecificNotice.setBotPaymentLiability(postbox: strongSelf.account.postbox, peerId: strongSelf.messageId.peerId).start() + strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) + } + })]), nil) + } + } + }) + } else { + self.inProgressDimNode.isUserInteractionEnabled = true + self.inProgressDimNode.alpha = 1.0 + self.actionButton.isEnabled = false + self.payDisposable.set((sendBotPaymentForm(account: self.account, messageId: self.messageId, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.inProgressDimNode.isUserInteractionEnabled = false + strongSelf.inProgressDimNode.alpha = 0.0 + strongSelf.actionButton.isEnabled = true + if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { + strongSelf.applePayAuthrorizationCompletion = nil + applePayAuthrorizationCompletion(.success) + } + if let applePayController = strongSelf.applePayController { + strongSelf.applePayController = nil + applePayController.presentingViewController?.dismiss(animated: true, completion: nil) + } + + switch result { + case .done: + strongSelf.dismissAnimated() + case let .externalVerificationRequired(url): + var dismissImpl: (() -> Void)? + let controller = BotCheckoutWebInteractionController(account: strongSelf.account, url: url, intent: .externalVerification({ _ in + dismissImpl?() + })) + dismissImpl = { [weak controller] in + controller?.dismiss() + self?.dismissAnimated() + } + strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.inProgressDimNode.isUserInteractionEnabled = false + strongSelf.inProgressDimNode.alpha = 0.0 + strongSelf.actionButton.isEnabled = true + if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { + strongSelf.applePayAuthrorizationCompletion = nil + applePayAuthrorizationCompletion(.failure) + } + if let applePayController = strongSelf.applePayController { + strongSelf.applePayController = nil + applePayController.presentingViewController?.dismiss(animated: true, completion: nil) + } + + let text: String + switch error { + case .precheckoutFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed + case .paymentFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed + case .alreadyPaid: + text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid + case .generic: + text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + } + + strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + } + })) + } + } + + private func requestPassword(cardTitle: String) { + let period: Int32 + let requiresBiometrics: Bool + if LocalAuth.isTouchIDAvailable { + period = 5 * 60 * 60 + requiresBiometrics = true + } else { + period = 1 * 60 * 60 + requiresBiometrics = false + } + self.present(botCheckoutPasswordEntryController(account: self.account, strings: self.presentationData.strings, cartTitle: cardTitle, period: period, requiresBiometrics: requiresBiometrics, completion: { [weak self] token in + if let strongSelf = self { + let durationString = timeIntervalString(strings: strongSelf.presentationData.strings, value: period) + + let alertText: String + if requiresBiometrics { + alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeoutAndTouchId(durationString).0 + } else { + alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeout(durationString).0 + } + + strongSelf.present(standardTextAlertController(title: nil, text: alertText, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_No, action: { + if let strongSelf = self { + strongSelf.pay(savedCredentialsToken: token) + } + }), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + if let strongSelf = self { + let _ = cacheTwoStepPasswordToken(postbox: strongSelf.account.postbox, token: token).start() + strongSelf.pay(savedCredentialsToken: token) + } + }) + ]), nil) + } + }), nil) + } + + func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) { + guard let paymentForm = self.paymentFormValue else { + completion(.failure) + return + } + guard let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" else { + completion(.failure) + return + } + guard let paramsData = nativeProvider.params.data(using: .utf8) else { + return + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { + return + } + guard let publishableKey = nativeParams["publishable_key"] as? String else { + return + } + + let signal: Signal = Signal { subscriber in + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + + let apiClient = STPAPIClient(configuration: configuration) + + apiClient.createToken(with: payment, completion: { token, error in + if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } else if let error = error { + subscriber.putError(error) + } + }) + + return ActionDisposable { + } + } + + self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self { + strongSelf.applePayAuthrorizationCompletion = completion + strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false)) + } else { + completion(.failure) + } + }, error: { _ in + completion(.failure) + })) + } + + func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) { + controller.presentingViewController?.dismiss(animated: true, completion: nil) + self.paymentAuthDisposable.set(nil) + } +} diff --git a/TelegramUI/BotCheckoutHeaderItem.swift b/TelegramUI/BotCheckoutHeaderItem.swift new file mode 100644 index 0000000000..abff88a68a --- /dev/null +++ b/TelegramUI/BotCheckoutHeaderItem.swift @@ -0,0 +1,278 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +class BotCheckoutHeaderItem: ListViewItem, ItemListItem { + let account: Account + let theme: PresentationTheme + let invoice: TelegramMediaInvoice + let botName: String + let sectionId: ItemListSectionId + + init(account: Account, theme: PresentationTheme, invoice: TelegramMediaInvoice, botName: String, sectionId: ItemListSectionId) { + self.account = account + self.theme = theme + self.invoice = invoice + self.botName = botName + self.sectionId = sectionId + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = BotCheckoutHeaderItemNode() + let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? BotCheckoutHeaderItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + let selectable: Bool = false +} + +private let titleFont = Font.semibold(16.0) +private let textFont = Font.regular(14.0) + +class BotCheckoutHeaderItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let imageNode: TransformImageNode + private let titleNode: TextNode + private let textNode: TextNode + private let botNameNode: TextNode + + private var item: BotCheckoutHeaderItem? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.imageNode = TransformImageNode() + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.textNode = TextNode() + self.textNode.isLayerBacked = true + self.textNode.contentMode = .left + self.textNode.contentsScale = UIScreen.main.scale + + self.botNameNode = TextNode() + self.botNameNode.isLayerBacked = true + self.botNameNode.contentMode = .left + self.botNameNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.imageNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.botNameNode) + } + + func asyncLayout() -> (_ item: BotCheckoutHeaderItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeBotNameLayout = TextNode.asyncLayout(self.botNameNode) + let makeImageLayout = self.imageNode.asyncLayout() + + let currentItem = self.item + + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let previousPhoto = currentItem?.invoice.photo + var imageUpdated = false + if let previousPhoto = previousPhoto, let photo = item.invoice.photo { + if !previousPhoto.isEqual(photo) { + imageUpdated = true + } + } else if (previousPhoto != nil) != (item.invoice.photo != nil) { + imageUpdated = true + } + + let textColor = item.theme.list.itemPrimaryTextColor + + let contentInsets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + let separatorHeight = UIScreenPixel + let titleTextSpacing: CGFloat = 1.0 + let textBotNameSpacing: CGFloat = 3.0 + let imageTextSpacing: CGFloat = 15.0 + + let imageSize = CGSize(width: 134.0, height: 134.0) + + let maxTextHeight = imageSize.height + var maxTextWidth = width - contentInsets.left - contentInsets.right + + var imageApply: (() -> Void)? + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if let photo = item.invoice.photo, let dimensions = photo.dimensions { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimensions.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) + imageApply = makeImageLayout(arguments) + maxTextWidth = max(1.0, maxTextWidth - imageSize.width - imageTextSpacing) + if imageUpdated { + updatedImageSignal = chatWebFileImage(account: item.account, file: photo) + } + } + + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.invoice.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + let (botNameLayout, botNameApply) = makeBotNameLayout(NSAttributedString(string: item.botName, font: textFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.invoice.description, font: textFont, textColor: textColor), nil, 0, .end, CGSize(width: maxTextWidth, height: maxTextHeight - titleLayout.size.height - titleTextSpacing - botNameLayout.size.height - textBotNameSpacing), .natural, nil, UIEdgeInsets()) + + let contentHeight: CGFloat + if let _ = imageApply { + contentHeight = contentInsets.top + contentInsets.bottom + imageSize.height + } else { + contentHeight = contentInsets.top + contentInsets.bottom + titleLayout.size.height + titleTextSpacing + textLayout.size.height + textBotNameSpacing + botNameLayout.size.height + } + + let contentSize = CGSize(width: width, height: contentHeight) + let insets = itemListNeighborsPlainInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + let _ = textApply() + let _ = botNameApply() + + if let imageApply = imageApply { + let _ = imageApply() + if let updatedImageSignal = updatedImageSignal { + strongSelf.imageNode.setSignal(account: item.account, signal: updatedImageSignal) + } + strongSelf.imageNode.isHidden = false + } else { + strongSelf.imageNode.isHidden = true + } + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: imageSize) + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: width, height: separatorHeight)) + + var titleFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: titleLayout.size) + if let _ = imageApply { + titleFrame.origin.x += imageSize.width + imageTextSpacing + } + strongSelf.titleNode.frame = titleFrame + + let textFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleTextSpacing), size: textLayout.size) + strongSelf.textNode.frame = textFrame + + strongSelf.botNameNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + textBotNameSpacing), size: botNameLayout.size) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/BotCheckoutInfoController.swift b/TelegramUI/BotCheckoutInfoController.swift new file mode 100644 index 0000000000..e10817dcd5 --- /dev/null +++ b/TelegramUI/BotCheckoutInfoController.swift @@ -0,0 +1,147 @@ +import Foundation +import SwiftSignalKit +import Display +import TelegramCore +import Postbox + +enum BotCheckoutInfoControllerFocus { + case address + case name + case phone + case email +} + +final class BotCheckoutInfoController: ViewController { + private var controllerNode: BotCheckoutInfoControllerNode { + return super.displayNode as! BotCheckoutInfoControllerNode + } + + private let account: Account + private let invoice: BotPaymentInvoice + private let messageId: MessageId + private let initialFormInfo: BotPaymentRequestedInfo + private let focus: BotCheckoutInfoControllerFocus + + private let formInfoUpdated: (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + private var doneItem: UIBarButtonItem? + private var activityItem: UIBarButtonItem? + + public init(account: Account, invoice: BotPaymentInvoice, messageId: MessageId, initialFormInfo: BotPaymentRequestedInfo, focus: BotCheckoutInfoControllerFocus, formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void) { + self.account = account + self.invoice = invoice + self.messageId = messageId + self.initialFormInfo = initialFormInfo + self.focus = focus + self.formInfoUpdated = formInfoUpdated + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + + self.title = self.presentationData.strings.CheckoutInfo_Title + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.rightBarButtonItem = self.doneItem + self.doneItem?.isEnabled = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = BotCheckoutInfoControllerNode(account: self.account, invoice: self.invoice, messageId: self.messageId, formInfo: self.initialFormInfo, focus: self.focus, theme: self.presentationData.theme, strings: self.presentationData.strings, dismiss: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }, openCountrySelection: { [weak self] in + if let strongSelf = self { + let controller = AuthorizationSequenceCountrySelectionController(displayCodes: false) + controller.completeWithCountryCode = { _, id in + if let strongSelf = self { + strongSelf.controllerNode.updateCountry(id) + } + } + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }, updateStatus: { [weak self] status in + if let strongSelf = self { + switch status { + case .notReady: + strongSelf.doneItem?.isEnabled = false + case .ready: + strongSelf.doneItem?.isEnabled = true + case .verifying: + break + } + switch status { + case .verifying: + if strongSelf.activityItem == nil { + strongSelf.activityItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: strongSelf.presentationData.theme)) + strongSelf.navigationItem.setRightBarButton(strongSelf.activityItem, animated: false) + } + default: + if strongSelf.activityItem != nil { + strongSelf.activityItem = nil + strongSelf.navigationItem.setRightBarButton(strongSelf.doneItem, animated: false) + } + } + } + }, formInfoUpdated: { [weak self] formInfo, validatedInfo in + if let strongSelf = self { + strongSelf.formInfoUpdated(formInfo, validatedInfo) + strongSelf.dismiss() + } + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + + self.displayNodeDidLoad() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func cancelPressed() { + self.dismiss() + } + + @objc func donePressed() { + self.controllerNode.verify() + } + + override open func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } +} diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift new file mode 100644 index 0000000000..3765bef34a --- /dev/null +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -0,0 +1,429 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +private final class BotCheckoutInfoAddressItems { + let address1: BotPaymentFieldItemNode + let address2: BotPaymentFieldItemNode + let city: BotPaymentFieldItemNode + let state: BotPaymentFieldItemNode + let country: BotPaymentDisclosureItemNode + let postcode: BotPaymentFieldItemNode + + var items: [BotPaymentItemNode] { + return [ + self.address1, + self.address2, + self.city, + self.state, + self.country, + self.postcode + ] + } + + init(strings: PresentationStrings, openCountrySelection: @escaping () -> Void) { + self.address1 = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoAddress1, placeholder: strings.CheckoutInfo_ShippingInfoAddress1Placeholder) + self.address2 = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoAddress2, placeholder: strings.CheckoutInfo_ShippingInfoAddress2Placeholder) + self.city = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoCity, placeholder: strings.CheckoutInfo_ShippingInfoCityPlaceholder) + self.state = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoState, placeholder: strings.CheckoutInfo_ShippingInfoStatePlaceholder) + self.country = BotPaymentDisclosureItemNode(title: strings.CheckoutInfo_ShippingInfoCountry, placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") + self.postcode = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoPostcode, placeholder: strings.CheckoutInfo_ShippingInfoPostcodePlaceholder) + + self.country.action = { + openCountrySelection() + } + } +} + +private final class BotCheckoutInfoControllerScrollerNodeView: UIScrollView { + var ignoreUpdateBounds = false + + override var bounds: CGRect { + get { + return super.bounds + } set(value) { + if !self.ignoreUpdateBounds { + super.bounds = value + } + } + } + + override func scrollRectToVisible(_ rect: CGRect, animated: Bool) { + } +} + +private final class BotCheckoutInfoControllerScrollerNode: ASDisplayNode { + override var view: BotCheckoutInfoControllerScrollerNodeView { + return super.view as! BotCheckoutInfoControllerScrollerNodeView + } + + override init() { + super.init(viewBlock: { + return BotCheckoutInfoControllerScrollerNodeView() + }, didLoad: nil) + } +} + +enum BotCheckoutInfoControllerStatus { + case notReady + case ready + case verifying +} + +final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let account: Account + private let invoice: BotPaymentInvoice + private let messageId: MessageId + private var focus: BotCheckoutInfoControllerFocus? + + private let dismiss: () -> Void + private let openCountrySelection: () -> Void + private let updateStatus: (BotCheckoutInfoControllerStatus) -> Void + private let formInfoUpdated: (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void + private let present: (ViewController, Any?) -> Void + + private var theme: PresentationTheme + private var strings: PresentationStrings + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let scrollNode: BotCheckoutInfoControllerScrollerNode + private let itemNodes: [[BotPaymentItemNode]] + + private let addressItems: BotCheckoutInfoAddressItems? + private let nameItem: BotPaymentFieldItemNode? + private let emailItem: BotPaymentFieldItemNode? + private let phoneItem: BotPaymentFieldItemNode? + private let saveInfoItem: BotPaymentSwitchItemNode + + private var formInfo: BotPaymentRequestedInfo + + private let verifyDisposable = MetaDisposable() + private var isVerifying = false + + init(account: Account, invoice: BotPaymentInvoice, messageId: MessageId, formInfo: BotPaymentRequestedInfo, focus: BotCheckoutInfoControllerFocus, theme: PresentationTheme, strings: PresentationStrings, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutInfoControllerStatus) -> Void, formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.account = account + self.invoice = invoice + self.messageId = messageId + self.formInfo = formInfo + self.focus = focus + self.dismiss = dismiss + self.openCountrySelection = openCountrySelection + self.updateStatus = updateStatus + self.formInfoUpdated = formInfoUpdated + self.present = present + + self.theme = theme + self.strings = strings + + self.scrollNode = BotCheckoutInfoControllerScrollerNode() + + var itemNodes: [[BotPaymentItemNode]] = [] + + var openCountrySelectionImpl: (() -> Void)? + + if invoice.requestedFields.contains(.shippingAddress) { + var sectionItems: [BotPaymentItemNode] = [] + let addressItems = BotCheckoutInfoAddressItems(strings: strings, openCountrySelection: { openCountrySelectionImpl?() + }) + + addressItems.address1.text = formInfo.shippingAddress?.streetLine1 ?? "" + addressItems.address2.text = formInfo.shippingAddress?.streetLine2 ?? "" + addressItems.city.text = formInfo.shippingAddress?.city ?? "" + addressItems.state.text = formInfo.shippingAddress?.state ?? "" + if let iso2 = formInfo.shippingAddress?.countryIso2, let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2.uppercased()) { + addressItems.country.text = name + } + addressItems.postcode.text = formInfo.shippingAddress?.postCode ?? "" + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.CheckoutInfo_ShippingInfoTitle)) + sectionItems.append(contentsOf: addressItems.items) + itemNodes.append(sectionItems) + self.addressItems = addressItems + } else { + self.addressItems = nil + } + + if !invoice.requestedFields.intersection([.name, .phone, .email]).isEmpty { + var sectionItems: [BotPaymentItemNode] = [] + sectionItems.append(BotPaymentHeaderItemNode(text: strings.CheckoutInfo_ReceiverInfoTitle)) + if invoice.requestedFields.contains(.name) { + let nameItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoName, placeholder: strings.CheckoutInfo_ReceiverInfoNamePlaceholder) + nameItem.text = formInfo.name ?? "" + self.nameItem = nameItem + sectionItems.append(nameItem) + } else { + self.nameItem = nil + } + if invoice.requestedFields.contains(.email) { + let emailItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoEmail, placeholder: strings.CheckoutInfo_ReceiverInfoEmailPlaceholder) + emailItem.text = formInfo.email ?? "" + self.emailItem = emailItem + sectionItems.append(emailItem) + } else { + self.emailItem = nil + } + if invoice.requestedFields.contains(.phone) { + let phoneItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoPhone, placeholder: strings.CheckoutInfo_ReceiverInfoPhone) + phoneItem.text = formInfo.phone ?? "" + self.phoneItem = phoneItem + sectionItems.append(phoneItem) + } else { + self.phoneItem = nil + } + itemNodes.append(sectionItems) + } else { + self.nameItem = nil + self.emailItem = nil + self.phoneItem = nil + } + + self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.CheckoutInfo_SaveInfo, isOn: true) + itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.CheckoutInfo_SaveInfoHelp)]) + + self.itemNodes = itemNodes + + for items in itemNodes { + for item in items { + self.scrollNode.addSubnode(item) + } + } + + super.init() + + self.backgroundColor = self.theme.list.blocksBackgroundColor + self.scrollNode.backgroundColor = nil + self.scrollNode.isOpaque = false + self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.delegate = self + + self.addSubnode(self.scrollNode) + + openCountrySelectionImpl = { [weak self] in + if let strongSelf = self { + strongSelf.view.endEditing(true) + strongSelf.openCountrySelection() + } + } + + for items in itemNodes { + for item in items { + if let item = item as? BotPaymentFieldItemNode { + item.textUpdated = { [weak self] in + self?.updateDone() + } + } + } + } + + self.updateDone() + } + + deinit { + self.verifyDisposable.dispose() + } + + func updateCountry(_ iso2: String) { + if self.formInfo.shippingAddress?.countryIso2 != iso2, let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2) { + let shippingAddress: BotPaymentShippingAddress + if let current = self.formInfo.shippingAddress { + shippingAddress = current + } else { + shippingAddress = BotPaymentShippingAddress(streetLine1: "", streetLine2: "", city: "", state: "", countryIso2: iso2, postCode: "") + } + + self.formInfo = BotPaymentRequestedInfo(name: self.formInfo.name, phone: self.formInfo.phone, email: self.formInfo.email, shippingAddress: BotPaymentShippingAddress(streetLine1: shippingAddress.streetLine1, streetLine2: shippingAddress.streetLine2, city: shippingAddress.city, state: shippingAddress.state, countryIso2: iso2, postCode: shippingAddress.postCode)) + self.addressItems?.country.text = name + if let containerLayout = self.containerLayout { + self.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + + self.updateDone() + } + } + + private func collectFormInfo() -> BotPaymentRequestedInfo { + var address: BotPaymentShippingAddress? + if let addressItems = self.addressItems, let current = self.formInfo.shippingAddress { + address = BotPaymentShippingAddress(streetLine1: addressItems.address1.text, streetLine2: addressItems.address2.text, city: addressItems.city.text, state: addressItems.state.text, countryIso2: current.countryIso2, postCode: addressItems.postcode.text) + } + return BotPaymentRequestedInfo(name: self.nameItem?.text, phone: self.phoneItem?.text, email: self.emailItem?.text, shippingAddress: address) + } + + func verify() { + self.isVerifying = true + let formInfo = self.collectFormInfo() + self.verifyDisposable.set((validateBotPaymentForm(network: self.account.network, saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.formInfoUpdated(formInfo, result) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.isVerifying = false + strongSelf.updateDone() + + let text: String + switch error { + case .shippingNotAvailable: + text = strongSelf.strings.CheckoutInfo_ErrorShippingNotAvailable + case .addressStateInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorStateInvalid + case .addressPostcodeInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorPostcodeInvalid + case .addressCityInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorCityInvalid + case .nameInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorNameInvalid + case .emailInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorEmailInvalid + case .phoneInvalid: + text = strongSelf.strings.CheckoutInfo_ErrorPhoneInvalid + case .generic: + text = strongSelf.strings.Login_UnknownError + } + + strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) + } + })) + + self.updateDone() + } + + private func updateDone() { + var enabled = true + if let addressItems = self.addressItems { + if addressItems.address1.text.isEmpty { + enabled = false + } + if addressItems.city.text.isEmpty { + enabled = false + } + if let shippingAddress = self.formInfo.shippingAddress, shippingAddress.countryIso2.isEmpty { + enabled = false + } + if addressItems.postcode.text.isEmpty { + enabled = false + } + } + if let nameItem = self.nameItem, nameItem.text.isEmpty { + enabled = false + } + if let phoneItem = self.phoneItem, phoneItem.text.isEmpty { + enabled = false + } + if let emailItem = self.emailItem, emailItem.text.isEmpty { + enabled = false + } + if self.isVerifying { + self.updateStatus(.verifying) + } else if enabled { + self.updateStatus(.ready) + } else { + self.updateStatus(.notReady) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let previousLayout = self.containerLayout + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + + var contentHeight: CGFloat = 0.0 + + var commonInset: CGFloat = 0.0 + for items in self.itemNodes { + for item in items { + commonInset = max(commonInset, item.measureInset(theme: self.theme, width: layout.size.width)) + } + } + + for items in self.itemNodes { + if !items.isEmpty && items[0] is BotPaymentHeaderItemNode { + contentHeight += 24.0 + } else { + contentHeight += 32.0 + } + + for i in 0 ..< items.count { + let item = items[i] + let itemHeight = item.updateLayout(theme: self.theme, width: layout.size.width, measuredInset: commonInset, previousItemNode: i == 0 ? nil : items[i - 1], nextItemNode: i == (items.count - 1) ? nil : items[i + 1], transition: transition) + transition.updateFrame(node: item, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: itemHeight))) + contentHeight += itemHeight + } + } + + contentHeight += 24.0 + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + + let previousBoundsOrigin = self.scrollNode.bounds.origin + self.scrollNode.view.ignoreUpdateBounds = true + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.scrollNode.view.contentSize = scrollContentSize + self.scrollNode.view.contentInset = insets + self.scrollNode.view.scrollIndicatorInsets = insets + self.scrollNode.view.ignoreUpdateBounds = false + + if let focus = focus { + var focusItem: ASDisplayNode? + switch focus { + case .address: + focusItem = self.addressItems?.address1 + case .name: + focusItem = self.nameItem + case .email: + focusItem = self.emailItem + case .phone: + focusItem = self.phoneItem + } + if let focusItem = focusItem { + let scrollVisibleSize = CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom) + var contentOffset = CGPoint(x: 0.0, y: -insets.top + floor(focusItem.frame.midY - scrollVisibleSize.height / 2.0)) + contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height) + contentOffset.y = max(contentOffset.y, -insets.top) + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + + if previousLayout == nil, let focusItem = focusItem as? BotPaymentFieldItemNode { + focusItem.activateInput() + } + } + } else if let previousLayout = previousLayout { + var previousInsets = previousLayout.0.insets(options: [.input]) + previousInsets.top += max(previousLayout.1, previousLayout.0.insets(options: [.statusBar]).top) + let insetsScrollOffset = insets.top - previousInsets.top + + var contentOffset = CGPoint(x: 0.0, y: previousBoundsOrigin.y + insetsScrollOffset) + contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height) + contentOffset.y = max(contentOffset.y, -insets.top) + + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + } else { + let contentOffset = CGPoint(x: 0.0, y: -insets.top) + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + } + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss() + } + completion?() + }) + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.focus = nil + } +} diff --git a/TelegramUI/BotCheckoutNativeCardEntryController.swift b/TelegramUI/BotCheckoutNativeCardEntryController.swift new file mode 100644 index 0000000000..04a720c9b8 --- /dev/null +++ b/TelegramUI/BotCheckoutNativeCardEntryController.swift @@ -0,0 +1,147 @@ +import Foundation +import SwiftSignalKit +import Display +import TelegramCore +import Postbox + +enum BotCheckoutNativeCardEntryStatus { + case notReady + case ready + case verifying +} + +struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let cardholderName = BotCheckoutNativeCardEntryAdditionalFields(rawValue: 1 << 0) + static let country = BotCheckoutNativeCardEntryAdditionalFields(rawValue: 1 << 1) + static let zipCode = BotCheckoutNativeCardEntryAdditionalFields(rawValue: 1 << 2) +} + +final class BotCheckoutNativeCardEntryController: ViewController { + private var controllerNode: BotCheckoutNativeCardEntryControllerNode { + return super.displayNode as! BotCheckoutNativeCardEntryControllerNode + } + + private let account: Account + private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields + private let publishableKey: String + private let completion: (BotCheckoutPaymentMethod) -> Void + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + private var doneItem: UIBarButtonItem? + private var activityItem: UIBarButtonItem? + + public init(account: Account, additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + self.account = account + self.additionalFields = additionalFields + self.publishableKey = publishableKey + self.completion = completion + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + + self.title = self.presentationData.strings.Checkout_NewCard_Title + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.rightBarButtonItem = self.doneItem + self.doneItem?.isEnabled = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = BotCheckoutNativeCardEntryControllerNode(additionalFields: self.additionalFields, publishableKey: self.publishableKey, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, dismiss: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }, openCountrySelection: { [weak self] in + if let strongSelf = self { + let controller = AuthorizationSequenceCountrySelectionController(displayCodes: false) + controller.completeWithCountryCode = { _, id in + if let strongSelf = self { + strongSelf.controllerNode.updateCountry(id) + } + } + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }, updateStatus: { [weak self] status in + if let strongSelf = self { + switch status { + case .notReady: + strongSelf.doneItem?.isEnabled = false + case .ready: + strongSelf.doneItem?.isEnabled = true + case .verifying: + break + } + switch status { + case .verifying: + if strongSelf.activityItem == nil { + strongSelf.activityItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: strongSelf.presentationData.theme)) + strongSelf.navigationItem.setRightBarButton(strongSelf.activityItem, animated: false) + } + default: + if strongSelf.activityItem != nil { + strongSelf.activityItem = nil + strongSelf.navigationItem.setRightBarButton(strongSelf.doneItem, animated: false) + } + } + } + }, completion: { [weak self] method in + self?.completion(method) + }) + + self.displayNodeDidLoad() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func cancelPressed() { + self.dismiss() + } + + @objc func donePressed() { + self.controllerNode.verify() + } + + override open func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } +} diff --git a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift new file mode 100644 index 0000000000..8077614ead --- /dev/null +++ b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift @@ -0,0 +1,355 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +import TelegramUIPrivateModule + +private final class BotCheckoutNativeCardEntryScrollerNodeView: UIScrollView { + var ignoreUpdateBounds = false + + override var bounds: CGRect { + get { + return super.bounds + } set(value) { + if !self.ignoreUpdateBounds { + super.bounds = value + } + } + } + + override func scrollRectToVisible(_ rect: CGRect, animated: Bool) { + } +} + +private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode { + override var view: BotCheckoutNativeCardEntryScrollerNodeView { + return super.view as! BotCheckoutNativeCardEntryScrollerNodeView + } + + override init() { + super.init(viewBlock: { + return BotCheckoutNativeCardEntryScrollerNodeView() + }, didLoad: nil) + } +} + +final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let publishableKey: String + + private let present: (ViewController, Any?) -> Void + private let dismiss: () -> Void + private let openCountrySelection: () -> Void + private let updateStatus: (BotCheckoutNativeCardEntryStatus) -> Void + private let completion: (BotCheckoutPaymentMethod) -> Void + + private var theme: PresentationTheme + private var strings: PresentationStrings + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let scrollNode: BotCheckoutNativeCardEntryScrollerNode + private let itemNodes: [[BotPaymentItemNode]] + + private let cardItem: BotPaymentCardInputItemNode + private let cardholderItem: BotPaymentFieldItemNode? + private let countryItem: BotPaymentDisclosureItemNode? + private let zipCodeItem: BotPaymentFieldItemNode? + + private let saveInfoItem: BotPaymentSwitchItemNode + + private let verifyDisposable = MetaDisposable() + private var isVerifying = false + + private var currentCardData: BotPaymentCardInputData? + private var currentCountryIso2: String? + + init(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + self.publishableKey = publishableKey + + self.present = present + self.dismiss = dismiss + self.openCountrySelection = openCountrySelection + self.updateStatus = updateStatus + self.completion = completion + + self.theme = theme + self.strings = strings + + self.scrollNode = BotCheckoutNativeCardEntryScrollerNode() + + var itemNodes: [[BotPaymentItemNode]] = [] + + var cardUpdatedImpl: ((BotPaymentCardInputData?) -> Void)? + var openCountrySelectionImpl: (() -> Void)? + + self.cardItem = BotPaymentCardInputItemNode() + cardItem.updated = { data in + cardUpdatedImpl?(data) + } + itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem]) + + if additionalFields.contains(.cardholderName) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) + + let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder) + self.cardholderItem = cardholderItem + sectionItems.append(cardholderItem) + + itemNodes.append(sectionItems) + } else { + self.cardholderItem = nil + } + + if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) + + if additionalFields.contains(.country) { + let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") + countryItem.action = { + openCountrySelectionImpl?() + } + self.countryItem = countryItem + sectionItems.append(countryItem) + } else { + self.countryItem = nil + } + if additionalFields.contains(.zipCode) { + let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder) + self.zipCodeItem = zipCodeItem + sectionItems.append(zipCodeItem) + } else { + self.zipCodeItem = nil + } + + itemNodes.append(sectionItems) + } else { + self.countryItem = nil + self.zipCodeItem = nil + } + + self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: true) + itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_SaveInfoHelp)]) + + self.itemNodes = itemNodes + + for items in itemNodes { + for item in items { + self.scrollNode.addSubnode(item) + } + } + + super.init() + + self.backgroundColor = self.theme.list.blocksBackgroundColor + self.scrollNode.backgroundColor = nil + self.scrollNode.isOpaque = false + self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.delegate = self + + self.addSubnode(self.scrollNode) + + cardUpdatedImpl = { [weak self] data in + if let strongSelf = self { + strongSelf.currentCardData = data + strongSelf.updateDone() + } + } + + openCountrySelectionImpl = { [weak self] in + if let strongSelf = self { + strongSelf.view.endEditing(true) + strongSelf.openCountrySelection() + } + } + + for items in itemNodes { + for item in items { + if let item = item as? BotPaymentFieldItemNode { + item.textUpdated = { [weak self] in + self?.updateDone() + } + } + } + } + + self.updateDone() + } + + deinit { + self.verifyDisposable.dispose() + } + + func updateCountry(_ iso2: String) { + if let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2) { + self.currentCountryIso2 = iso2 + self.countryItem?.text = name + if let containerLayout = self.containerLayout { + self.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + + self.updateDone() + } + } + + func verify() { + guard let cardData = self.currentCardData else { + return + } + + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = self.publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + + let apiClient = STPAPIClient(configuration: configuration) + + let card = STPCardParams() + card.number = cardData.number + card.cvc = cardData.code + card.expYear = cardData.year + card.expMonth = cardData.month + card.name = self.cardholderItem?.text + card.addressCountry = self.currentCountryIso2 + card.addressZip = self.zipCodeItem?.text + + let createToken: Signal = Signal { subscriber in + apiClient.createToken(withCard: card, completion: { token, error in + if let error = error { + subscriber.putError(error) + } else if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } + }) + + return ActionDisposable { + let _ = apiClient.publishableKey + } + } + + self.isVerifying = true + self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self, let card = token.card { + let last4 = card.last4() + let brand = STPAPIClient.string(with: card.brand) + strongSelf.completion(.webToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn)) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.isVerifying = false + strongSelf.updateDone() + } + })) + + self.updateDone() + } + + private func updateDone() { + var enabled = true + + if self.currentCardData == nil { + enabled = false + } + + if let cardholderItem = self.cardholderItem, cardholderItem.text.isEmpty { + enabled = false + } + + if let _ = self.countryItem, self.currentCountryIso2 == nil { + enabled = false + } + + if let zipCodeItem = self.zipCodeItem, zipCodeItem.text.isEmpty { + enabled = false + } + + if self.isVerifying { + self.updateStatus(.verifying) + } else if enabled { + self.updateStatus(.ready) + } else { + self.updateStatus(.notReady) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let previousLayout = self.containerLayout + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + + var contentHeight: CGFloat = 0.0 + + var commonInset: CGFloat = 0.0 + for items in self.itemNodes { + for item in items { + commonInset = max(commonInset, item.measureInset(theme: self.theme, width: layout.size.width)) + } + } + + for items in self.itemNodes { + if !items.isEmpty && items[0] is BotPaymentHeaderItemNode { + contentHeight += 24.0 + } else { + contentHeight += 32.0 + } + + for i in 0 ..< items.count { + let item = items[i] + let itemHeight = item.updateLayout(theme: self.theme, width: layout.size.width, measuredInset: commonInset, previousItemNode: i == 0 ? nil : items[i - 1], nextItemNode: i == (items.count - 1) ? nil : items[i + 1], transition: transition) + transition.updateFrame(node: item, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: itemHeight))) + contentHeight += itemHeight + } + } + + contentHeight += 24.0 + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + + let previousBoundsOrigin = self.scrollNode.bounds.origin + self.scrollNode.view.ignoreUpdateBounds = true + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.scrollNode.view.contentSize = scrollContentSize + self.scrollNode.view.contentInset = insets + self.scrollNode.view.scrollIndicatorInsets = insets + self.scrollNode.view.ignoreUpdateBounds = false + + if let previousLayout = previousLayout { + var previousInsets = previousLayout.0.insets(options: [.input]) + previousInsets.top += max(previousLayout.1, previousLayout.0.insets(options: [.statusBar]).top) + let insetsScrollOffset = insets.top - previousInsets.top + + var contentOffset = CGPoint(x: 0.0, y: previousBoundsOrigin.y + insetsScrollOffset) + contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height) + contentOffset.y = max(contentOffset.y, -insets.top) + + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + } else { + let contentOffset = CGPoint(x: 0.0, y: -insets.top) + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + } + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss() + } + completion?() + }) + } +} diff --git a/TelegramUI/BotCheckoutPasswordEntryController.swift b/TelegramUI/BotCheckoutPasswordEntryController.swift new file mode 100644 index 0000000000..349c15c196 --- /dev/null +++ b/TelegramUI/BotCheckoutPasswordEntryController.swift @@ -0,0 +1,309 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit + +private struct BotCheckoutPasswordAlertAction { + public let title: String + public let action: () -> Void + + public init(title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } +} + +private final class BotCheckoutPasswordAlertActionNode: HighlightableButtonNode { + private let backgroundNode: ASDisplayNode + + let action: BotCheckoutPasswordAlertAction + + init(action: BotCheckoutPasswordAlertAction) { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = UIColor(rgb: 0xe0e5e6) + self.backgroundNode.alpha = 0.0 + + self.action = action + + super.init() + + self.setTitle(action.title, with: Font.regular(17.0), with: UIColor(rgb: 0x007ee5), for: []) + self.setTitle(action.title, with: Font.regular(17.0), with: UIColor(rgb: 0xb3b3b3), for: [.disabled]) + + self.highligthedChanged = { [weak self] value in + if let strongSelf = self { + if value { + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else if !strongSelf.backgroundNode.alpha.isZero { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + } + + override func didLoad() { + super.didLoad() + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc func pressed() { + self.action.action() + } + + override func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + } +} + +private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { + private let account: Account + private let period: Int32 + private let requiresBiometrics: Bool + private let completion: (TemporaryTwoStepPasswordToken) -> Void + + private let titleNode: ASTextNode + private let textNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [BotCheckoutPasswordAlertActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let cancelActionNode: BotCheckoutPasswordAlertActionNode + private let doneActionNode: BotCheckoutPasswordAlertActionNode + + private let textFieldNodeBackground: ASImageNode + private let textFieldNode: TextFieldNode + + private var validLayout: CGSize? + private var isVerifying = false + private let disposable = MetaDisposable() + + private let hapticFeedback = HapticFeedback() + + init(account: Account, strings: PresentationStrings, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) { + self.account = account + self.period = period + self.requiresBiometrics = requiresBiometrics + self.completion = completion + + let titleNode = ASTextNode() + titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: .black, paragraphAlignment: .center) + titleNode.displaysAsynchronously = false + titleNode.isLayerBacked = true + titleNode.maximumNumberOfLines = 1 + titleNode.truncationMode = .byTruncatingTail + self.titleNode = titleNode + + self.textNode = ASTextNode() + self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).0, font: Font.regular(13.0), textColor: .black, paragraphAlignment: .center) + self.textNode.displaysAsynchronously = false + self.textNode.isLayerBacked = true + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + self.actionNodesSeparator.backgroundColor = UIColor(rgb: 0xc9cdd7) + + self.cancelActionNode = BotCheckoutPasswordAlertActionNode(action: BotCheckoutPasswordAlertAction(title: strings.Common_Cancel, action: { + cancel() + })) + + var doneImpl: (() -> Void)? + self.doneActionNode = BotCheckoutPasswordAlertActionNode(action: BotCheckoutPasswordAlertAction(title: strings.Checkout_PasswordEntry_Pay, action: { + doneImpl?() + })) + + self.actionNodes = [self.cancelActionNode, self.doneActionNode] + + var actionVerticalSeparators: [ASDisplayNode] = [] + if self.actionNodes.count > 1 { + for _ in 0 ..< self.actionNodes.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + separatorNode.backgroundColor = UIColor(rgb: 0xc9cdd7) + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + self.textFieldNodeBackground = ASImageNode() + self.textFieldNodeBackground.displaysAsynchronously = false + self.textFieldNodeBackground.displayWithoutProcessing = true + self.textFieldNodeBackground.image = generateImage(CGSize(width: 4.0, height: 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.black.cgColor) + context.setLineWidth(UIScreenPixel) + context.stroke(CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 2) + + self.textFieldNode = TextFieldNode() + self.textFieldNode.textField.textColor = .black + self.textFieldNode.textField.font = Font.regular(12.0) + self.textFieldNode.textField.typingAttributes = [NSFontAttributeName: Font.regular(12.0)] + self.textFieldNode.textField.isSecureTextEntry = true + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.addSubnode(self.textFieldNodeBackground) + self.addSubnode(self.textFieldNode) + + self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + + self.updateState() + + doneImpl = { [weak self] in + self?.verify() + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let previousLayout = self.validLayout + self.validLayout = size + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + + let actionsHeight: CGFloat = 44.0 + + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionsHeight)) + minActionsWidth += actionTitleSize.width + actionTitleInsets + } + + let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth) + + let spacing: CGFloat = 6.0 + let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) + transition.updateFrame(node: titleNode, frame: titleFrame) + + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let inputHeight: CGFloat = 38.0 + + let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + inputHeight) + + let textFieldBackgroundFrame = CGRect(origin: CGPoint(x: insets.left, y: resultSize.height - inputHeight + 12.0 - actionsHeight - insets.bottom), size: CGSize(width: resultSize.width - insets.left - insets.right, height: 25.0)) + self.textFieldNodeBackground.frame = textFieldBackgroundFrame + self.textFieldNode.frame = textFieldBackgroundFrame.offsetBy(dx: 0.0, dy: 1.0).insetBy(dx: 4.0, dy: 0.0) + + self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + + let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) + + actionOffset += currentActionWidth + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if previousLayout == nil { + self.textFieldNode.textField.becomeFirstResponder() + } + + return resultSize + } + + @objc func textFieldChanged(_ textField: UITextField) { + self.updateState() + } + + private func updateState() { + var enabled = true + + if self.isVerifying { + enabled = false + } + + if let text = self.textFieldNode.textField.text { + if text.isEmpty { + enabled = false + } + } else { + enabled = false + } + + self.doneActionNode.isEnabled = enabled + } + + private func verify() { + guard let text = self.textFieldNode.textField.text, !text.isEmpty else { + return + } + + self.isVerifying = true + self.disposable.set((requestTemporaryTwoStepPasswordToken(account: self.account, password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self { + strongSelf.completion(token) + } + }, error: { [weak self] _ in + if let strongSelf = self { + strongSelf.textFieldNodeBackground.layer.addShakeAnimation() + strongSelf.textFieldNode.layer.addShakeAnimation() + strongSelf.hapticFeedback.error() + strongSelf.isVerifying = false + strongSelf.updateState() + } + })) + self.updateState() + } +} + +func botCheckoutPasswordEntryController(account: Account, strings: PresentationStrings, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { + var dismissImpl: (() -> Void)? + let controller = AlertController(contentNode: BotCheckoutPasswordAlertContentNode(account: account, strings: strings, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { + dismissImpl?() + }, completion: { token in + completion(token) + dismissImpl?() + })) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + return controller +} diff --git a/TelegramUI/BotCheckoutPaymentMethodSheet.swift b/TelegramUI/BotCheckoutPaymentMethodSheet.swift new file mode 100644 index 0000000000..222610ffcd --- /dev/null +++ b/TelegramUI/BotCheckoutPaymentMethodSheet.swift @@ -0,0 +1,258 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +enum BotCheckoutPaymentMethod: Equatable { + case savedCredentials(BotPaymentSavedCredentials) + case webToken(title: String, data: String, saveOnServer: Bool) + case applePayStripe + + static func ==(lhs: BotCheckoutPaymentMethod, rhs: BotCheckoutPaymentMethod) -> Bool { + switch lhs { + case let .savedCredentials(credentials): + if case .savedCredentials(credentials) = rhs { + return true + } else { + return false + } + case let .webToken(title, data, saveOnServer): + if case .webToken(title, data, saveOnServer) = rhs { + return true + } else { + return false + } + case .applePayStripe: + if case .applePayStripe = rhs { + return true + } else { + return false + } + } + } + + var title: String { + switch self { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, title): + return title + } + case let .webToken(title, _, _): + return title + case .applePayStripe: + return "Apple Pay" + } + } +} + +final class BotCheckoutPaymentMethodSheetController: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + super.init() + + var items: [ActionSheetItem] = [] + + items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod)) + + for method in methods { + let title: String + let icon: UIImage? + switch method { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, cardTitle): + title = cardTitle + icon = nil + } + case let .webToken(webTitle, _, _): + title = webTitle + icon = nil + case .applePayStripe: + title = "Apple Pay" + icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed() + } + let value: Bool? + if let currentMethod = currentMethod { + value = method == currentMethod + } else { + value = nil + } + items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in + applyValue(method) + self?.dismissAnimated() + })) + } + + items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in + self?.dismissAnimated() + newCard() + })) + + self.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + self?.dismissAnimated() + }), + ]) + ]) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +public class BotCheckoutPaymentMethodItem: ActionSheetItem { + public let title: String + public let icon: UIImage? + public let value: Bool? + public let action: (Bool) -> Void + + public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) { + self.title = title + self.icon = icon + self.value = value + self.action = action + } + + public func node() -> ActionSheetItemNode { + let node = BotCheckoutPaymentMethodItemNode() + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? BotCheckoutPaymentMethodItemNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +private let checkIcon = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(rgb: 0x007ee5).cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() +}) + +public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { + public static let defaultFont: UIFont = Font.regular(20.0) + + private var item: BotCheckoutPaymentMethodItem? + + private let button: HighlightTrackingButton + private let titleNode: ASTextNode + private let iconNode: ASImageNode + private let checkNode: ASImageNode + + public override init() { + self.button = HighlightTrackingButton() + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + self.checkNode = ASImageNode() + self.checkNode.isUserInteractionEnabled = false + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.image = checkIcon + + super.init() + + self.view.addSubview(self.button) + self.addSubnode(self.titleNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.checkNode) + + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + }) + } + } + } + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + func setItem(_ item: BotCheckoutPaymentMethodItem) { + self.item = item + + self.titleNode.attributedText = NSAttributedString(string: item.title, font: BotCheckoutPaymentMethodItemNode.defaultFont, textColor: .black) + self.iconNode.image = item.icon + if let value = item.value { + self.checkNode.isHidden = !value + } else { + self.checkNode.isHidden = true + } + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + var checkInset: CGFloat = 15.0 + if let _ = self.item?.value { + checkInset = 44.0 + } + + let iconSize: CGSize + if let image = self.iconNode.image { + iconSize = image.size + } else { + iconSize = CGSize() + } + let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + } + } + + @objc func buttonPressed() { + if let item = self.item { + let updatedValue: Bool + if let value = item.value { + updatedValue = !value + } else { + updatedValue = true + } + item.action(updatedValue) + } + } +} diff --git a/TelegramUI/BotCheckoutPaymentShippingOptionSheetController.swift b/TelegramUI/BotCheckoutPaymentShippingOptionSheetController.swift new file mode 100644 index 0000000000..1f8785ea00 --- /dev/null +++ b/TelegramUI/BotCheckoutPaymentShippingOptionSheetController.swift @@ -0,0 +1,213 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) { + self.theme = theme + self.strings = strings + + super.init() + + var items: [ActionSheetItem] = [] + + items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod)) + + let dismissAction: () -> Void = { [weak self] in + self?.dismissAnimated() + } + + let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in + for i in 0 ..< options.count { + self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in + if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value { + return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action) + } + return item + }) + } + applyValue(id) + dismissAction() + } + + var itemIndex = 0 + for option in options { + let index = itemIndex + var totalPrice: Int64 = 0 + for price in option.prices { + totalPrice += price.amount + } + let value: Bool? + if let currentId = currentId { + value = option.id == currentId + } else { + value = nil + } + items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in + toggleCheck(option.id, index) + })) + itemIndex += 1 + } + + self.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + self?.dismissAnimated() + }), + ]) + ]) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private let checkIcon = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(rgb: 0x007ee5).cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() +}) + +public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem { + public let title: String + public let label: String + public let value: Bool? + public let action: (Bool) -> Void + + public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) { + self.title = title + self.label = label + self.value = value + self.action = action + } + + public func node() -> ActionSheetItemNode { + let node = BotCheckoutPaymentShippingOptionItemNode() + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else { + assertionFailure() + return + } + + node.setItem(self) + } +} + +public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { + public static let defaultFont: UIFont = Font.regular(20.0) + + private var item: BotCheckoutPaymentShippingOptionItem? + + private let button: HighlightTrackingButton + private let titleNode: ASTextNode + private let labelNode: ASTextNode + private let checkNode: ASImageNode + + public override init() { + self.button = HighlightTrackingButton() + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.labelNode = ASTextNode() + self.labelNode.maximumNumberOfLines = 1 + self.labelNode.isUserInteractionEnabled = false + self.labelNode.displaysAsynchronously = false + + self.checkNode = ASImageNode() + self.checkNode.isUserInteractionEnabled = false + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.image = checkIcon + + super.init() + + self.view.addSubview(self.button) + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.checkNode) + + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + }) + } + } + } + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + func setItem(_ item: BotCheckoutPaymentShippingOptionItem) { + self.item = item + + self.titleNode.attributedText = NSAttributedString(string: item.title, font: BotCheckoutPaymentShippingOptionItemNode.defaultFont, textColor: .black) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: BotCheckoutPaymentShippingOptionItemNode.defaultFont, textColor: .black) + if let value = item.value { + self.checkNode.isHidden = !value + } else { + self.checkNode.isHidden = true + } + + self.setNeedsLayout() + } + + public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + public override func layout() { + super.layout() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + var checkInset: CGFloat = 15.0 + if let _ = self.item?.value { + checkInset = 44.0 + } + + let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height)) + let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + } + } + + @objc func buttonPressed() { + if let item = self.item { + let updatedValue: Bool + if let value = item.value { + updatedValue = !value + } else { + updatedValue = true + } + item.action(updatedValue) + } + } +} diff --git a/TelegramUI/BotCheckoutPriceItem.swift b/TelegramUI/BotCheckoutPriceItem.swift new file mode 100644 index 0000000000..6ff432b948 --- /dev/null +++ b/TelegramUI/BotCheckoutPriceItem.swift @@ -0,0 +1,146 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class BotCheckoutPriceItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let title: String + let label: String + let isFinal: Bool + let sectionId: ItemListSectionId + + let requestsNoInset: Bool = true + + init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, sectionId: ItemListSectionId) { + self.theme = theme + self.title = title + self.label = label + self.isFinal = isFinal + self.sectionId = sectionId + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = BotCheckoutPriceItemNode() + let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? BotCheckoutPriceItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + let selectable: Bool = false +} + +private let titleFont = Font.regular(17.0) +private let finalFont = Font.semibold(17.0) + +private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets { + var insets = UIEdgeInsets() + switch neighbors.top { + case .otherSection: + insets.top += 8.0 + case .none, .sameSection: + break + } + switch neighbors.bottom { + case .none, .otherSection: + insets.bottom += 8.0 + case .sameSection: + break + } + return insets +} + +class BotCheckoutPriceItemNode: ListViewItemNode { + let titleNode: TextNode + let labelNode: TextNode + + private var item: BotCheckoutPriceItem? + + init() { + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + + self.labelNode = TextNode() + self.labelNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + } + + func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + return { item, width, neighbors in + let rightInset: CGFloat = 16.0 + + let contentSize = CGSize(width: width, height: 34.0) + let insets = priceItemInsets(neighbors) + + let textFont: UIFont + let textColor: UIColor + if item.isFinal { + textFont = finalFont + textColor = item.theme.list.itemPrimaryTextColor + } else { + textFont = titleFont + textColor = item.theme.list.itemSecondaryTextColor + } + + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: textFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: textFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let _ = titleApply() + let _ = labelApply() + + let leftInset: CGFloat = 16.0 + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/BotCheckoutWebInteractionController.swift b/TelegramUI/BotCheckoutWebInteractionController.swift new file mode 100644 index 0000000000..8ab030bc72 --- /dev/null +++ b/TelegramUI/BotCheckoutWebInteractionController.swift @@ -0,0 +1,83 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +enum BotCheckoutWebInteractionControllerIntent { + case addPaymentMethod((BotCheckoutPaymentMethod) -> Void) + case externalVerification((Bool) -> Void) +} + +final class BotCheckoutWebInteractionController: ViewController { + private var controllerNode: BotCheckoutWebInteractionControllerNode { + return self.displayNode as! BotCheckoutWebInteractionControllerNode + } + + private let account: Account + private let url: String + private let intent: BotCheckoutWebInteractionControllerIntent + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + init(account: Account, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + self.account = account + self.url = url + self.intent = intent + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + switch intent { + case .addPaymentMethod: + self.title = self.presentationData.strings.Checkout_NewCard_Title + case .externalVerification: + self.title = self.presentationData.strings.Checkout_WebConfirmation_Title + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func cancelPressed() { + if case let .externalVerification(completion) = self.intent { + completion(false) + } + self.dismiss() + } + + override func loadDisplayNode() { + self.displayNode = BotCheckoutWebInteractionControllerNode(presentationData: self.presentationData, url: self.url, intent: self.intent) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + self.controllerNode.animateIn() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/BotCheckoutWebInteractionControllerNode.swift b/TelegramUI/BotCheckoutWebInteractionControllerNode.swift new file mode 100644 index 0000000000..b9f0487f41 --- /dev/null +++ b/TelegramUI/BotCheckoutWebInteractionControllerNode.swift @@ -0,0 +1,140 @@ +import Foundation +import Display +import AsyncDisplayKit +import WebKit + +private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + +final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate { + private var presentationData: PresentationData + private let intent: BotCheckoutWebInteractionControllerIntent + + private var webView: WKWebView? + + init(presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + self.presentationData = presentationData + self.intent = intent + + super.init() + + self.backgroundColor = .white + + let webView: WKWebView + switch intent { + case .addPaymentMethod: + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + + userController.add(WeakPaymentScriptMessageHandler { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) + } + }, name: "performAction") + + configuration.userContentController = userController + + webView = WKWebView(frame: CGRect(), configuration: configuration) + case .externalVerification: + webView = WKWebView() + webView.navigationDelegate = self + } + self.webView = webView + self.view.addSubview(webView) + + if let parsedUrl = URL(string: url) { + webView.load(URLRequest(url: parsedUrl)) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.webView?.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight))) + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in + completion?() + }) + } + + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["eventName"] as? String else { + return + } + + if eventName == "payment_form_submit" { + guard let eventString = body["eventData"] as? String else { + return + } + + guard let eventData = eventString.data(using: .utf8) else { + return + } + + guard let dict = (try? JSONSerialization.jsonObject(with: eventData, options: [])) as? [String: Any] else { + return + } + + guard let title = dict["title"] as? String else { + return + } + + guard let credentials = dict["credentials"] else { + return + } + + guard let credentialsData = try? JSONSerialization.data(withJSONObject: credentials, options: []) else { + return + } + + guard let credentialsString = String(data: credentialsData, encoding: .utf8) else { + return + } + + if case let .addPaymentMethod(completion) = self.intent { + completion(.webToken(title: title, data: credentialsString, saveOnServer: false)) + } + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if case let .externalVerification(completion) = self.intent, let host = navigationAction.request.url?.host { + if host == "t.me" || host == "telegram.me" { + decisionHandler(.cancel) + completion(true) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } +} diff --git a/TelegramUI/BotPaymentCardInputItemNode.swift b/TelegramUI/BotPaymentCardInputItemNode.swift new file mode 100644 index 0000000000..2ac57d471d --- /dev/null +++ b/TelegramUI/BotPaymentCardInputItemNode.swift @@ -0,0 +1,62 @@ +import Foundation +import AsyncDisplayKit +import Display + +import TelegramUIPrivateModule + +struct BotPaymentCardInputData { + let number: String + let code: String + let year: UInt + let month: UInt +} + +final class BotPaymentCardInputItemNode: BotPaymentItemNode, STPPaymentCardTextFieldDelegate { + private let cardField: STPPaymentCardTextField + + private var theme: PresentationTheme? + + var updated: ((BotPaymentCardInputData?) -> Void)? + + init() { + self.cardField = STPPaymentCardTextField() + self.cardField.borderColor = .clear + self.cardField.borderWidth = 0.0 + + super.init(needsBackground: true) + + self.cardField.delegate = self + self.view.addSubview(self.cardField) + } + + override func measureInset(theme: PresentationTheme, width: CGFloat) -> CGFloat { + return 0.0 + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + + self.cardField.textColor = theme.list.itemPrimaryTextColor + self.cardField.textErrorColor = theme.list.itemDestructiveColor + self.cardField.placeholderColor = theme.list.itemPlaceholderTextColor + self.cardField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance + } + + self.cardField.frame = CGRect(origin: CGPoint(x: 5.0, y: 0.0), size: CGSize(width: width - 10.0, height: 44.0)) + + return 44.0 + } + + func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { + if textField.isValid, let number = textField.cardParams.number, let code = textField.cardParams.cvc { + self.updated?(BotPaymentCardInputData(number: number, code: code, year: textField.cardParams.expYear, month: textField.cardParams.expMonth)) + } else { + self.updated?(nil) + } + } + + func activateInput() { + self.cardField.becomeFirstResponder() + } +} diff --git a/TelegramUI/BotPaymentDisclosureItemNode.swift b/TelegramUI/BotPaymentDisclosureItemNode.swift new file mode 100644 index 0000000000..8eb0ac45c6 --- /dev/null +++ b/TelegramUI/BotPaymentDisclosureItemNode.swift @@ -0,0 +1,126 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(17.0) + +final class BotPaymentDisclosureItemNode: BotPaymentItemNode { + private let title: String + private let placeholder: String + var text: String { + didSet { + if let theme = self.theme { + self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + } + } + } + + private let highlightedBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let textNode: ASTextNode + private let buttonNode: HighlightTrackingButtonNode + + private var theme: PresentationTheme? + + var action: (() -> Void)? + + init(title: String, placeholder: String, text: String) { + self.title = title + self.text = text + self.placeholder = placeholder + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.alpha = 0.0 + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 1 + + self.buttonNode = HighlightTrackingButtonNode() + + super.init(needsBackground: true) + + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let supernode = strongSelf.supernode { + supernode.view.bringSubview(toFront: strongSelf.view) + } + + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(buttonPressed), forControlEvents: .touchUpInside) + } + + override func measureInset(theme: PresentationTheme, width: CGFloat) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + if self.text.isEmpty { + self.textNode.attributedText = NSAttributedString(string: self.placeholder, font: titleFont, textColor: theme.list.itemPlaceholderTextColor) + } else { + self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + } + } + + let leftInset: CGFloat = 16.0 + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude)) + + if titleSize.width.isZero { + return 0.0 + } else { + return leftInset + titleSize.width + 17.0 + } + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + if self.text.isEmpty { + self.textNode.attributedText = NSAttributedString(string: self.placeholder, font: titleFont, textColor: theme.list.itemPlaceholderTextColor) + } else { + self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + } + } + + self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0)) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel))) + + let leftInset: CGFloat = 16.0 + + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize)) + + var textInset = leftInset + if !titleSize.width.isZero { + textInset += titleSize.width + 18.0 + } + textInset = max(measuredInset, textInset) + + let textSize = self.textNode.measure(CGSize(width: width - measuredInset - 8.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset, y: 11.0), size: textSize)) + + return 44.0 + } + + @objc func buttonPressed() { + self.action?() + } +} diff --git a/TelegramUI/BotPaymentFieldItemNode.swift b/TelegramUI/BotPaymentFieldItemNode.swift new file mode 100644 index 0000000000..e3c013f51b --- /dev/null +++ b/TelegramUI/BotPaymentFieldItemNode.swift @@ -0,0 +1,95 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(17.0) + +final class BotPaymentFieldItemNode: BotPaymentItemNode { + private let title: String + var text: String { + get { + return self.textField.textField.text ?? "" + } set(value) { + self.textField.textField.text = value + } + } + private let placeholder: String + private let titleNode: ASTextNode + + private let textField: TextFieldNode + + private var theme: PresentationTheme? + + var textUpdated: (() -> Void)? + + init(title: String, placeholder: String) { + self.title = title + self.placeholder = placeholder + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + + self.textField = TextFieldNode() + self.textField.textField.font = titleFont + self.textField.textField.returnKeyType = .next + + super.init(needsBackground: true) + + self.addSubnode(self.titleNode) + self.addSubnode(self.textField) + + self.textField.textField.addTarget(self, action: #selector(self.editingChanged), for: [.editingChanged]) + } + + override func measureInset(theme: PresentationTheme, width: CGFloat) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + self.textField.textField.textColor = theme.list.itemPrimaryTextColor + self.textField.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: titleFont, textColor: theme.list.itemPlaceholderTextColor) + self.textField.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance + } + + let leftInset: CGFloat = 16.0 + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude)) + + if titleSize.width.isZero { + return 0.0 + } else { + return leftInset + titleSize.width + 17.0 + } + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + self.textField.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance + } + + let leftInset: CGFloat = 16.0 + + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude)) + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize)) + + var textInset = leftInset + if !titleSize.width.isZero { + textInset += titleSize.width + 18.0 + } + + textInset = max(measuredInset, textInset) + + transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 3.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) + + return 44.0 + } + + func activateInput() { + self.textField.textField.becomeFirstResponder() + } + + @objc func editingChanged() { + self.textUpdated?() + } +} diff --git a/TelegramUI/BotPaymentHeaderItemNode.swift b/TelegramUI/BotPaymentHeaderItemNode.swift new file mode 100644 index 0000000000..5817676b23 --- /dev/null +++ b/TelegramUI/BotPaymentHeaderItemNode.swift @@ -0,0 +1,37 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(14.0) + +final class BotPaymentHeaderItemNode: BotPaymentItemNode { + private let text: String + private let textNode: ASTextNode + + private var theme: PresentationTheme? + + init(text: String) { + self.text = text + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 1 + + super.init(needsBackground: false) + + self.addSubnode(self.textNode) + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + } + + let leftInset: CGFloat = 16.0 + + let textSize = self.textNode.measure(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude)) + + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: textSize)) + + return 30.0 + } +} diff --git a/TelegramUI/BotPaymentItemNode.swift b/TelegramUI/BotPaymentItemNode.swift new file mode 100644 index 0000000000..d313913173 --- /dev/null +++ b/TelegramUI/BotPaymentItemNode.swift @@ -0,0 +1,66 @@ +import Foundation +import AsyncDisplayKit +import Display + +class BotPaymentItemNode: ASDisplayNode { + private let needsBackground: Bool + + let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + + private var theme: PresentationTheme? + + init(needsBackground: Bool) { + self.needsBackground = needsBackground + + self.backgroundNode = ASDisplayNode() + self.topSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode = ASDisplayNode() + + super.init() + + if needsBackground { + self.addSubnode(self.backgroundNode) + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + } + } + + func measureInset(theme: PresentationTheme, width: CGFloat) -> CGFloat { + return 0.0 + } + + final func updateLayout(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, previousItemNode: BotPaymentItemNode?, nextItemNode: BotPaymentItemNode?, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.backgroundNode.backgroundColor = theme.list.itemBackgroundColor + self.topSeparatorNode.backgroundColor = theme.list.itemSeparatorColor + self.bottomSeparatorNode.backgroundColor = theme.list.itemSeparatorColor + } + + let height = self.layoutContents(theme: theme, width: width, measuredInset: measuredInset, transition: transition) + + var topSeparatorInset: CGFloat = 0.0 + + if let previousItemNode = previousItemNode, previousItemNode.needsBackground { + topSeparatorInset = 16.0 + } + + if let nextItemNode = nextItemNode, nextItemNode.needsBackground { + bottomSeparatorNode.isHidden = true + } else { + bottomSeparatorNode.isHidden = false + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel))) + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + + return height + } + + func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + return 0.0 + } +} diff --git a/TelegramUI/BotPaymentSwitchItemNode.swift b/TelegramUI/BotPaymentSwitchItemNode.swift new file mode 100644 index 0000000000..84c1295b03 --- /dev/null +++ b/TelegramUI/BotPaymentSwitchItemNode.swift @@ -0,0 +1,60 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let titleFont = Font.regular(17.0) + +final class BotPaymentSwitchItemNode: BotPaymentItemNode { + private let title: String + private let titleNode: ASTextNode + private let switchNode: SwitchNode + + private var theme: PresentationTheme? + + var isOn: Bool { + get { + return self.switchNode.isOn + } set(value) { + if self.switchNode.isOn != value { + self.switchNode.setOn(value, animated: true) + } + } + } + + init(title: String, isOn: Bool) { + self.title = title + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 1 + + self.switchNode = SwitchNode() + self.switchNode.setOn(isOn, animated: false) + + super.init(needsBackground: true) + + self.addSubnode(self.titleNode) + self.addSubnode(self.switchNode) + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemPrimaryTextColor) + + self.switchNode.frameColor = theme.list.itemSwitchColors.frameColor + self.switchNode.contentColor = theme.list.itemSwitchColors.contentColor + self.switchNode.handleColor = theme.list.itemSwitchColors.handleColor + } + + let leftInset: CGFloat = 16.0 + + let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude)) + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize)) + + let switchSize = self.switchNode.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.switchNode, frame: CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: 6.0), size: switchSize)) + + return 44.0 + } +} diff --git a/TelegramUI/BotPaymentTextItemNode.swift b/TelegramUI/BotPaymentTextItemNode.swift new file mode 100644 index 0000000000..309270394e --- /dev/null +++ b/TelegramUI/BotPaymentTextItemNode.swift @@ -0,0 +1,38 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let textFont = Font.regular(14.0) + +final class BotPaymentTextItemNode: BotPaymentItemNode { + private let text: String + private let textNode: ASTextNode + + private var theme: PresentationTheme? + + init(text: String) { + self.text = text + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + super.init(needsBackground: false) + + self.addSubnode(self.textNode) + } + + override func layoutContents(theme: PresentationTheme, width: CGFloat, measuredInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + if self.theme !== theme { + self.theme = theme + self.textNode.attributedText = NSAttributedString(string: self.text, font: textFont, textColor: theme.list.sectionHeaderTextColor) + } + + let leftInset: CGFloat = 16.0 + + let textSize = self.textNode.measure(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude)) + + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: textSize)) + + return textSize.height + 7.0 + 7.0 + } + +} diff --git a/TelegramUI/BotReceiptController.swift b/TelegramUI/BotReceiptController.swift new file mode 100644 index 0000000000..687bd92d86 --- /dev/null +++ b/TelegramUI/BotReceiptController.swift @@ -0,0 +1,88 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class BotReceiptController: ViewController { + private var controllerNode: BotReceiptControllerNode { + return self.displayNode as! BotReceiptControllerNode + } + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + private let account: Account + private let invoice: TelegramMediaInvoice + private let messageId: MessageId + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + init(account: Account, invoice: TelegramMediaInvoice, messageId: MessageId) { + self.account = account + self.invoice = invoice + self.messageId = messageId + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + var title = self.presentationData.strings.Checkout_Receipt_Title + if invoice.flags.contains(.isTest) { + title += " (Test)" + } + self.title = title + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + let displayNode = BotReceiptControllerNode(updateNavigationOffset: { [weak self] offset in + if let strongSelf = self { + strongSelf.navigationOffset = offset + } + }, account: self.account, invoice: self.invoice, messageId: self.messageId, dismissAnimated: { [weak self] in + self?.dismiss() + }) + + displayNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.displayNode = displayNode + super.displayNodeDidLoad() + self._ready.set(displayNode.ready) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + @objc func cancelPressed() { + self.dismiss() + } +} diff --git a/TelegramUI/BotReceiptControllerNode.swift b/TelegramUI/BotReceiptControllerNode.swift new file mode 100644 index 0000000000..a203eaebba --- /dev/null +++ b/TelegramUI/BotReceiptControllerNode.swift @@ -0,0 +1,309 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +final class BotReceiptControllerArguments { + fileprivate let account: Account + + fileprivate init(account: Account) { + self.account = account + } +} + +private enum BotReceiptSection: Int32 { + case header + case prices + case info +} + +enum BotReceiptEntry: ItemListNodeEntry { + case header(PresentationTheme, TelegramMediaInvoice, String) + case price(Int, PresentationTheme, String, String, Bool) + case paymentMethod(PresentationTheme, String, String) + case shippingInfo(PresentationTheme, String, String) + case shippingMethod(PresentationTheme, String, String) + case nameInfo(PresentationTheme, String, String) + case emailInfo(PresentationTheme, String, String) + case phoneInfo(PresentationTheme, String, String) + + var section: ItemListSectionId { + switch self { + case .header: + return BotReceiptSection.header.rawValue + case .price: + return BotReceiptSection.prices.rawValue + default: + return BotReceiptSection.info.rawValue + } + } + + var stableId: Int32 { + switch self { + case .header: + return 0 + case let .price(index, _, _, _, _): + return 1 + Int32(index) + case .paymentMethod: + return 10000 + 0 + case .shippingInfo: + return 10000 + 1 + case .shippingMethod: + return 10000 + 2 + case .nameInfo: + return 10000 + 3 + case .emailInfo: + return 10000 + 4 + case .phoneInfo: + return 10000 + 5 + } + } + + static func ==(lhs: BotReceiptEntry, rhs: BotReceiptEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsInvoice, lhsName): + if case let .header(rhsTheme, rhsInvoice, rhsName) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if !lhsInvoice.isEqual(rhsInvoice) { + return false + } + if lhsName != rhsName { + return false + } + return true + } else { + return false + } + case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal): + if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsText != rhsText { + return false + } + if lhsValue != rhsValue { + return false + } + if lhsFinal != rhsFinal { + return false + } + return true + } else { + return false + } + case let .paymentMethod(lhsTheme, lhsText, lhsValue): + if case let .paymentMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shippingInfo(lhsTheme, lhsText, lhsValue): + if case let .shippingInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shippingMethod(lhsTheme, lhsText, lhsValue): + if case let .shippingMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .nameInfo(lhsTheme, lhsText, lhsValue): + if case let .nameInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .emailInfo(lhsTheme, lhsText, lhsValue): + if case let .emailInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .phoneInfo(lhsTheme, lhsText, lhsValue): + if case let .phoneInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + } + } + + static func <(lhs: BotReceiptEntry, rhs: BotReceiptEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: BotReceiptControllerArguments) -> ListViewItem { + switch self { + case let .header(theme, invoice, botName): + return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) + case let .price(_, theme, text, value, isFinal): + return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section) + case let .paymentMethod(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + case let .shippingInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + case let .shippingMethod(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + case let .nameInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + case let .emailInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + case let .phoneInfo(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + } + } +} + +private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?) -> [BotReceiptEntry] { + var entries: [BotReceiptEntry] = [] + + var botName = "" + if let botPeer = botPeer { + botName = botPeer.displayTitle + } + entries.append(.header(presentationData.theme, invoice, botName)) + + if let formInvoice = formInvoice { + var totalPrice: Int64 = 0 + + var index = 0 + for price in formInvoice.prices { + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false)) + totalPrice += price.amount + index += 1 + } + + var shippingOptionString: String? + if let shippingOption = shippingOption { + shippingOptionString = shippingOption.title + + for price in shippingOption.prices { + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false)) + totalPrice += price.amount + index += 1 + } + } + + entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true)) + + if let paymentMethodTitle = paymentMethodTitle { + entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle)) + } + + if formInvoice.requestedFields.contains(.shippingAddress) { + var addressString = "" + if let address = formInfo?.shippingAddress { + let components: [String] = [ + address.city, + address.streetLine1, + address.streetLine2, + address.state + ] + for component in components { + if !component.isEmpty { + if !addressString.isEmpty { + addressString.append(", ") + } + addressString.append(component) + } + } + } + entries.append(.shippingInfo(presentationData.theme, presentationData.strings.Checkout_ShippingAddress, addressString)) + + if let shippingOptionString = shippingOptionString { + entries.append(.shippingMethod(presentationData.theme, presentationData.strings.Checkout_ShippingMethod, shippingOptionString)) + } + } + + if formInvoice.requestedFields.contains(.name) { + entries.append(.nameInfo(presentationData.theme, presentationData.strings.Checkout_Name, formInfo?.name ?? "")) + } + + if formInvoice.requestedFields.contains(.email) { + entries.append(.emailInfo(presentationData.theme, presentationData.strings.Checkout_Email, formInfo?.email ?? "")) + } + + if formInvoice.requestedFields.contains(.phone) { + entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? "")) + } + } + + return entries +} + +private func availablePaymentMethods(current: BotCheckoutPaymentMethod?) -> [BotCheckoutPaymentMethod] { + if let current = current { + return [current] + } + return [] +} + +final class BotReceiptControllerNode: ItemListControllerNode { + private let account: Account + private let dismissAnimated: () -> Void + + private var presentationData: PresentationData + + private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?)?>(nil) + private var dataRequestDisposable: Disposable? + + private let actionButton: BotCheckoutActionButton + + init(updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) { + self.account = account + self.dismissAnimated = dismissAnimated + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let arguments = BotReceiptControllerArguments(account: account) + + let signal: Signal<(PresentationTheme, (ItemListNodeState, BotReceiptEntry.ItemGenerationArguments)), NoError> = combineLatest(account.telegramApplicationContext.presentationData, receiptData.get(), account.postbox.loadedPeerWithId(messageId.peerId)) + |> map { presentationData, receiptData, botPeer -> (PresentationTheme, (ItemListNodeState, BotReceiptEntry.ItemGenerationArguments)) in + let nodeState = ItemListNodeState(entries: botReceiptControllerEntries(presentationData: presentationData, invoice: invoice, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) + + return (presentationData.theme, (nodeState, arguments)) + } + + self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) + self.actionButton.setState(.inactive(self.presentationData.strings.Common_Done)) + + super.init(updateNavigationOffset: updateNavigationOffset, state: signal) + + self.dataRequestDisposable = (requestBotPaymentReceipt(network: account.network, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in + if let strongSelf = self { + strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle))) + } + }) + + self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.actionButton) + } + + deinit { + self.dataRequestDisposable?.dispose() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var updatedInsets = layout.intrinsicInsets + updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), navigationBarHeight: navigationBarHeight, transition: transition) + + let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) + transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) + self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) + } + + @objc func actionButtonPressed() { + self.dismissAnimated() + } +} diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index f665ab0c3f..5fd52afd96 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -215,7 +215,7 @@ public final class CallListController: ViewController { let _ = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) began?() } - })]), in: .window) + })]), in: .window(.root)) } }) } diff --git a/TelegramUI/ChangePhoneNumberCodeController.swift b/TelegramUI/ChangePhoneNumberCodeController.swift index 15ee880c6f..4850a20106 100644 --- a/TelegramUI/ChangePhoneNumberCodeController.swift +++ b/TelegramUI/ChangePhoneNumberCodeController.swift @@ -288,7 +288,7 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } dismissImpl = { [weak controller] in diff --git a/TelegramUI/ChangePhoneNumberController.swift b/TelegramUI/ChangePhoneNumberController.swift index 5d4077e8e6..55514996c0 100644 --- a/TelegramUI/ChangePhoneNumberController.swift +++ b/TelegramUI/ChangePhoneNumberController.swift @@ -68,14 +68,14 @@ final class ChangePhoneNumberController: ViewController { self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { let controller = AuthorizationSequenceCountrySelectionController() - controller.completeWithCountryCode = { code in + controller.completeWithCountryCode = { code, _ in if let strongSelf = self { strongSelf.updateData(countryCode: Int32(code), number: strongSelf.controllerNode.codeAndNumber.1) strongSelf.controllerNode.activateInput() } } strongSelf.controllerNode.view.endEditing(true) - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } } } @@ -123,7 +123,7 @@ final class ChangePhoneNumberController: ViewController { text = "An error occurred. Please try again later." } - strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } })) } else { diff --git a/TelegramUI/ChangePhoneNumberIntroController.swift b/TelegramUI/ChangePhoneNumberIntroController.swift index b9bb858752..b36bac97fc 100644 --- a/TelegramUI/ChangePhoneNumberIntroController.swift +++ b/TelegramUI/ChangePhoneNumberIntroController.swift @@ -132,6 +132,6 @@ final class ChangePhoneNumberIntroController: ViewController { if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(account: strongSelf.account), animated: true) } - })]), in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + })]), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 53106ba824..be0deeb0dd 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -670,7 +670,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index f25873a625..084f418e6b 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -381,7 +381,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index ffdd240cc7..74a1ceb4c8 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -760,7 +760,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value, presentationArguments in - controller?.present(value, in: .window, with: presentationArguments) + controller?.present(value, in: .window(.root), with: presentationArguments) } popToRootControllerImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) @@ -784,7 +784,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text })]) - strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let resultItemNode = resultItemNode { return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) } else { diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index 9d155df579..7f91a856d1 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -279,7 +279,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo result.set(true) }) ]) - contactsController.present(alertController, in: .window) + contactsController.present(alertController, in: .window(.root)) } return result.get() @@ -419,7 +419,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 0435ff5249..87d855521f 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -728,7 +728,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { UIPasteboard.general.string = text })]) - strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let resultItemNode = resultItemNode { return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) } else { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 6dd61d2f9c..fcc35065a8 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -138,7 +138,7 @@ public class ChatController: TelegramController { strongSelf.chatDisplayNode.dismissInput() strongSelf.present(legacyLocationController(message: message, mapMedia: mapMedia, account: strongSelf.account, openPeer: { peer in self?.openPeer(peerId: peer.id, navigation: .info, fromMessageId: nil) - }), in: .window) + }), in: .window(.root)) } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { for attribute in file.attributes { if case let .Sticker(_, reference) = attribute { @@ -148,14 +148,14 @@ public class ChatController: TelegramController { self?.controllerInteraction?.sendSticker(file) } strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } break } } } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo { 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, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + 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: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -164,7 +164,7 @@ public class ChatController: TelegramController { if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) } - }) + }, baseNavigationController: strongSelf.navigationController as? NavigationController) strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in if let strongSelf = strongSelf { @@ -182,7 +182,7 @@ public class ChatController: TelegramController { })) strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in + strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { var transitionNode: ASDisplayNode? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in @@ -216,7 +216,7 @@ public class ChatController: TelegramController { if let _ = galleryMedia { let gallery = SecretMediaPreviewController(account: strongSelf.account, messageId: messageId) strongSelf.secretMediaPreviewController = gallery - strongSelf.present(gallery, in: .window) + strongSelf.present(gallery, in: .window(.root)) } } }, closeSecretMessagePreview: { [weak self] in @@ -250,7 +250,7 @@ public class ChatController: TelegramController { } } - strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in if let node = node { return (node, frame) } else { @@ -363,7 +363,7 @@ public class ChatController: TelegramController { strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) case let .url(url): if isGame { - strongSelf.present(GameController(account: strongSelf.account, url: url, message: message), in: .window) + strongSelf.present(GameController(account: strongSelf.account, url: url, message: message), in: .window(.root)) } else { strongSelf.openUrl(url) } @@ -441,7 +441,7 @@ public class ChatController: TelegramController { }, defaultAction: ShareControllerAction(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, action: { copyLink?() })) - strongSelf.present(shareController, in: .window) + strongSelf.present(shareController, in: .window(.root)) shareAction = { [weak shareController] peerIds in shareController?.dismiss() @@ -462,7 +462,7 @@ public class ChatController: TelegramController { } } }, presentController: { [weak self] controller, arguments in - self?.present(controller, in: .window, with: arguments) + self?.present(controller, in: .window(.root), with: arguments) }, callPeer: { [weak self] peerId in if let strongSelf = self { let callResult = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) @@ -477,7 +477,7 @@ public class ChatController: TelegramController { if let strongSelf = self, let peer = peer, let current = current { strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) - })]), in: .window) + })]), in: .window(.root)) } }) } @@ -511,7 +511,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) case let .peerMention(peerId, mention): let actionSheet = ActionSheetController() var items: [ActionSheetItem] = [] @@ -535,7 +535,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) case let .mention(mention): let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -555,7 +555,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) case let .command(command): let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -575,7 +575,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(hashtag): let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -596,7 +596,21 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) + } + } + }, openCheckoutOrReceipt: { [weak self] messageId in + if let strongSelf = self { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + for media in message.media { + if let invoice = media as? TelegramMediaInvoice { + if let receiptMessageId = invoice.receiptMessageId { + strongSelf.present(BotReceiptController(account: strongSelf.account, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + strongSelf.present(BotCheckoutController(account: strongSelf.account, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + } } } }) @@ -822,6 +836,8 @@ public class ChatController: TelegramController { canReport = cachedData.reportStatus == .canReport } else if let cachedData = combinedInitialData.cachedData as? CachedGroupData { canReport = cachedData.reportStatus == .canReport + } else if let cachedData = combinedInitialData.cachedData as? CachedSecretChatData { + canReport = cachedData.reportStatus == .canReport } strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage).updatedPinnedMessageId(pinnedMessageId).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in if pinnedMessageId != nil { @@ -890,6 +906,8 @@ public class ChatController: TelegramController { canReport = cachedData.reportStatus == .canReport } else if let cachedData = cachedData as? CachedGroupData { canReport = cachedData.reportStatus == .canReport + } else if let cachedData = cachedData as? CachedSecretChatData { + canReport = cachedData.reportStatus == .canReport } if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || strongSelf.presentationInterfaceState.canReportPeer != canReport { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in @@ -1081,7 +1099,7 @@ public class ChatController: TelegramController { }, openContacts: { if let strongSelf = self { let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.DialogList_SelectContact }) - strongSelf.present(contactsController, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) strongSelf.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let peerId = peerId { let peer = strongSelf.account.postbox.loadedPeerWithId(peerId) @@ -1120,13 +1138,13 @@ public class ChatController: TelegramController { legacyController?.dismiss() } - strongSelf.present(legacyController, in: .window) + strongSelf.present(legacyController, in: .window(.root)) controller.present(in: emptyController, sourceView: nil, animated: true) presentOverlayController = { [weak legacyController] controller in if let legacyController = legacyController { let childController = LegacyController(legacyController: controller, presentation: .custom) - legacyController.present(childController, in: .window) + legacyController.present(childController, in: .window(.root)) return { [weak childController] in childController?.dismiss() } @@ -1235,7 +1253,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) } })) } @@ -1243,7 +1261,7 @@ public class ChatController: TelegramController { }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { //let controller = ShareRecipientsActionSheetController() - //strongSelf.present(controller, in: .window) + //strongSelf.present(controller, in: .window(.root)) if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { let forwardMessageIds = Array(forwardMessageIdsSet).sorted() @@ -1281,7 +1299,7 @@ public class ChatController: TelegramController { } } } - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } } }, updateTextInputState: { [weak self] f in @@ -1444,7 +1462,7 @@ public class ChatController: TelegramController { })) } }) - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } }, navigateToMessage: { [weak self] messageId in self?.navigateToMessage(from: nil, to: messageId) @@ -1501,7 +1519,7 @@ public class ChatController: TelegramController { let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start() } }) - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } } }, sendSticker: { [weak self] file in @@ -1538,7 +1556,7 @@ public class ChatController: TelegramController { pinAction(false) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { pinAction(true) - })]), in: .window) + })]), in: .window(.root)) } else { if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -1565,7 +1583,7 @@ public class ChatController: TelegramController { } disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .clear).start()) } - })]), in: .window) + })]), in: .window(.root)) } else { if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -1582,7 +1600,7 @@ public class ChatController: TelegramController { self?.dismissReportPeer() }, deleteChat: { [weak self] in self?.deleteChat(reportChatSpam: false) - }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) + }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { @@ -1662,7 +1680,7 @@ public class ChatController: TelegramController { self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings) self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in - self?.recentlyUsedInlineBotsValue = peers + self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) }) } @@ -1877,7 +1895,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - self.present(actionSheet, in: .window) + self.present(actionSheet, in: .window(.root)) case .openChatInfo: self.navigationActionDisposable.set((self.peerView.get() |> take(1) @@ -1904,7 +1922,7 @@ public class ChatController: TelegramController { presentOverlayController = { [weak legacyController] controller in if let legacyController = legacyController { let childController = LegacyController(legacyController: controller, presentation: .custom) - legacyController.present(childController, in: .window) + legacyController.present(childController, in: .window(.root)) return { [weak childController] in childController?.dismiss() } @@ -1927,7 +1945,7 @@ public class ChatController: TelegramController { legacyController.dismiss() } } - strongSelf.present(legacyController, in: .window) + strongSelf.present(legacyController, in: .window(.root)) } }) } @@ -1946,7 +1964,7 @@ public class ChatController: TelegramController { let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue), replyToMessageId: replyMessageId) let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [message]).start() } - }), in: .window) + }), in: .window(.root)) } private func enqueueMediaMessages(signals: [Any]?) { @@ -2187,7 +2205,7 @@ public class ChatController: TelegramController { } } } - self.present(controller, in: .window) + self.present(controller, in: .window(.root)) } case let .withBotStartPayload(_): break @@ -2246,7 +2264,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) - self.present(actionSheet, in: .window) + self.present(actionSheet, in: .window(.root)) } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 7e5af84c33..347fdf3b91 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -64,8 +64,9 @@ public final class ChatControllerInteraction { let presentController: (ViewController, Any?) -> Void let callPeer: (PeerId) -> Void let longTap: (ChatControllerInteractionLongTapAction) -> Void + let openCheckoutOrReceipt: (MessageId) -> Void - public init(openMessage: @escaping (MessageId) -> Void, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -90,5 +91,6 @@ public final class ChatControllerInteraction { self.presentController = presentController self.callPeer = callPeer self.longTap = longTap + self.openCheckoutOrReceipt = openCheckoutOrReceipt } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 257f37f2c9..495550c2ef 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -295,7 +295,7 @@ class ChatControllerNode: ASDisplayNode { } insets.top += navigationBarHeight - transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 44.0))) + transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 50.0))) var titleAccessoryPanelFrame: CGRect? if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 0931d72411..1e1ea49438 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -59,8 +59,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD switch state { case .waitingForNetwork: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true) - case .connecting: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Connecting, activity: true) + case let .connecting(toProxy): + strongSelf.titleView.title = NetworkStatusTitle(text: toProxy ? strongSelf.presentationData.strings.State_ConnectingToProxy : strongSelf.presentationData.strings.State_Connecting, activity: true) case .updating: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true) case .online: @@ -164,6 +164,12 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self?.activateSearch() } + self.chatListDisplayNode.chatListNode.presentAlert = { [weak self] text in + if let strongSelf = self { + self?.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + } + self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in if let strongSelf = self { let actionSheet = ActionSheetController() @@ -180,7 +186,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 874b469188..b662f06937 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -456,6 +456,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { messageText = text.string } } + case _ as TelegramMediaExpiredContent: + if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { + messageText = text.string + } default: break } @@ -464,6 +468,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { peer = item.peer.chatMainPeer messageText = "" + if item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { + if let secretChat = item.peer.peers[item.peer.peerId] as? TelegramSecretChat { + switch secretChat.embeddedState { + case .active: + messageText = item.strings.Notification_EncryptedChatAccepted + case .terminated: + messageText = item.strings.DialogList_EncryptionRejected + case .handshake: + switch secretChat.role { + case .creator: + messageText = item.strings.Notification_EncryptedChatRequested + case .participant: + messageText = item.strings.DialogList_EncryptionProcessing + } + } + } + } } let attributedText: NSAttributedString diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index fa4b9694d8..98c985807d 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -157,6 +157,7 @@ final class ChatListNode: ListView { var peerSelected: ((PeerId) -> Void)? var activateSearch: (() -> Void)? var deletePeerChat: ((PeerId) -> Void)? + var presentAlert: ((String) -> Void)? private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? @@ -202,7 +203,16 @@ final class ChatListNode: ListView { } } }, setPeerPinned: { peerId, _ in - let _ = togglePeerChatPinned(postbox: account.postbox, peerId: peerId).start() + let _ = (togglePeerChatPinned(postbox: account.postbox, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case .done: + break + case .limitExceeded: + strongSelf.presentAlert?(strongSelf.currentState.strings.DialogList_PinLimitError("5").0) + } + } + }) }, setPeerMuted: { peerId, _ in let _ = togglePeerMuted(account: account, peerId: peerId).start() }, deletePeer: { [weak self] peerId in diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index fa620822a7..6a9efb6155 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -140,6 +140,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsPeer != rhsPeer { return false } + return true default: return false diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index d9bd33755d..7a53f8303e 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -405,9 +405,15 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { |> mapToSignal { [weak self] peers, themeAndStrings -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] entries.append(.topPeers([], themeAndStrings.0, themeAndStrings.1)) + var peerIds = Set() var index = 0 - for renderedPeer in peers { + loop: for renderedPeer in peers { if let peer = renderedPeer.peers[renderedPeer.peerId] { + if peerIds.contains(peer.id) { + continue loop + } + peerIds.insert(peer.id) + var associatedPeer: Peer? if let associatedPeerId = peer.associatedPeerId { associatedPeer = renderedPeer.peers[associatedPeerId] diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index 31e096409e..3bc2aae7dc 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -1,6 +1,7 @@ import Foundation import AsyncDisplayKit import TelegramCore +import Postbox import Display private let titleFont = Font.medium(16.0) @@ -52,13 +53,25 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ theme: PresentationTheme, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) - return { theme, button, constrainedWidth, position in + return { theme, strings, message, button, constrainedWidth, position in let sideInset: CGFloat = 8.0 let minimumSideInset: CGFloat = 4.0 - let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: theme.chat.bubble.actionButtonsTextColor), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + var title = button.title + if case .payment = button.action { + for media in message.media { + if let invoice = media as? TelegramMediaInvoice { + if invoice.receiptMessageId != nil { + title = strings.Message_ReplyActionButtonShowReceipt + } + } + } + } + + let (titleSize, titleApply) = titleLayout(NSAttributedString(string: title, font: titleFont, textColor: theme.chat.bubble.actionButtonsTextColor), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundImage: UIImage? switch position { @@ -119,12 +132,10 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - //(_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (maxWidth: CGFloat, layout: (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) - - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ theme: PresentationTheme, _ replyMarkup: ReplyMarkupMessageAttribute, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] - return { theme, replyMarkup, constrainedWidth in + return { theme, strings, replyMarkup, message, constrainedWidth in let buttonHeight: CGFloat = 42.0 let buttonSpacing: CGFloat = 4.0 @@ -157,9 +168,9 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { - prepareButtonLayout = currentButtonLayouts[buttonIndex](theme, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = currentButtonLayouts[buttonIndex](theme, strings, message, button, maximumButtonWidth, buttonPosition) } else { - prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(theme, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(theme, strings, message, button, maximumButtonWidth, buttonPosition) } maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 9d66c583e6..11f9b3cb2e 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -283,6 +283,51 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings var argumentAttributes = peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)]) argumentAttributes[1] = MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: argumentAttributes) + case let .paymentSent(currency, totalAmount): + var invoiceMessage: Message? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + invoiceMessage = message + } + } + + var invoiceTitle: String? + if let invoiceMessage = invoiceMessage { + for media in invoiceMessage.media { + if let invoice = media as? TelegramMediaInvoice { + invoiceTitle = invoice.title + } + } + } + + if let invoiceTitle = invoiceTitle { + let botString: String + if let peer = messageMainPeer(message) { + botString = peer.compactDisplayTitle + } else { + botString = "" + } + let mutableString = NSMutableAttributedString() + mutableString.append(NSAttributedString(string: strings.Notification_PaymentSent, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor)) + + var range = NSRange(location: NSNotFound, length: 0) + + range = (mutableString.string as NSString).range(of: "{amount}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor)) + } + range = (mutableString.string as NSString).range(of: "{name}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: botString, font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor)) + } + range = (mutableString.string as NSString).range(of: "{title}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: invoiceTitle, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor)) + } + attributedString = mutableString + } else { + attributedString = NSAttributedString(string: strings.Message_PaymentSent(formatCurrencyAmount(totalAmount, currency: currency)).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } case .phoneCall: break default: @@ -290,6 +335,13 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings } break + } else if let expiredMedia = media as? TelegramMediaExpiredContent { + switch expiredMedia.data { + case .image: + attributedString = NSAttributedString(string: strings.Message_ImageExpired, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + case .file: + attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index a892e96a87..830c98ae75 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -462,7 +462,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.theme, replyMarkup, maximumNodeWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.theme, item.strings, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -1166,7 +1166,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil) } case .payment: - break + controllerInteraction.openCheckoutOrReceipt(item.message.id) } } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 82ac1bbdac..31efabb756 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -6,9 +6,7 @@ import Postbox import TelegramCore class ChatMessageInstantVideoItemNode: ChatMessageItemView { - let backgroundNode: ASImageNode - let videoNode: ManagedVideoNode - var progressNode: RadialProgressNode? + var hostedVideoNode: InstantVideoNode? var tapRecognizer: UITapGestureRecognizer? private var selectionNode: ChatMessageSelectionNode? @@ -26,25 +24,23 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private let playbackStatusDisposable = MetaDisposable() + private var shouldAcquireVideoContext: Bool { + if case .visible = self.visibility { + return true + } else { + return false + } + } + override var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { - if let item = self.item, let telegramFile = self.telegramFile, case .visible = self.visibility { - self.videoNode.acquireContext(account: item.account, mediaManager: item.account.telegramApplicationContext.mediaManager, id: PeerMessageManagedMediaId(messageId: item.message.id), resource: telegramFile.resource, priority: 1) - } else { - self.videoNode.discardContext() - } + self.hostedVideoNode?.setShouldAcquireContext(self.shouldAcquireVideoContext) } } } required init() { - self.backgroundNode = ASImageNode() - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - - self.videoNode = ManagedVideoNode() - self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.muteIconNode = ASImageNode() self.muteIconNode.isLayerBacked = true @@ -53,8 +49,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { super.init(layerBacked: false) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.videoNode) self.addSubnode(self.dateAndStatusNode) self.addSubnode(self.muteIconNode) } @@ -93,11 +87,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in var updatedTheme: PresentationTheme? - var updatedBackgroundImage: UIImage? var updatedMuteIconImage: UIImage? if item.theme !== currentItem?.theme { updatedTheme = item.theme - updatedBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.theme) updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.theme) } @@ -216,21 +208,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if let strongSelf = self { strongSelf.appliedItem = item - if let updatedBackgroundImage = updatedBackgroundImage { - strongSelf.backgroundNode.image = updatedBackgroundImage - } - if let updatedMuteIconImage = updatedMuteIconImage { strongSelf.muteIconNode.image = updatedMuteIconImage } strongSelf.telegramFile = updatedFile - strongSelf.videoNode.frame = videoFrame - strongSelf.videoNode.transformArguments = arguments - - strongSelf.backgroundNode.frame = videoFrame.insetBy(dx: -2.0, dy: -2.0) - if let image = strongSelf.muteIconNode.image { strongSelf.muteIconNode.frame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - image.size.width) / 2.0), y: videoFrame.maxY - image.size.height - 8.0), size: image.size) } @@ -268,11 +251,31 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { dateAndStatusApply(false) strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 70.0, width - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) - if let telegramFile = updatedFile, updatedMedia, let context = item.account.applicationContext as? TelegramApplicationContext, strongSelf.visibility == .visible { - strongSelf.videoNode.acquireContext(account: item.account, mediaManager: context.mediaManager, id: PeerMessageManagedMediaId(messageId: item.message.id), resource: telegramFile.resource, priority: 1) + if let telegramFile = updatedFile, updatedMedia, let context = item.account.applicationContext as? TelegramApplicationContext { + if let hostedVideoNode = strongSelf.hostedVideoNode { + hostedVideoNode.removeFromSupernode() + } + let hostedVideoNode = InstantVideoNode(theme: item.theme, manager: context.mediaManager, account: item.account, source: .messageMedia(stableId: item.message.stableId, file: telegramFile), priority: 1, withSound: false) + hostedVideoNode.tapped = { + if let strongSelf = self { + if let item = strongSelf.item { + if strongSelf.muteIconNode.alpha.isZero { + item.account.telegramApplicationContext.mediaManager.playlistPlayerControl(.stop) + } else { + strongSelf.controllerInteraction?.openMessage(item.message.id) + } + } + } + } + strongSelf.hostedVideoNode = hostedVideoNode + strongSelf.insertSubnode(hostedVideoNode, belowSubnode: strongSelf.dateAndStatusNode) + hostedVideoNode.setShouldAcquireContext(strongSelf.shouldAcquireVideoContext) } - strongSelf.progressNode?.position = strongSelf.videoNode.position + if let hostedVideoNode = strongSelf.hostedVideoNode { + hostedVideoNode.frame = videoFrame + hostedVideoNode.updateLayout(arguments.boundingSize) + } if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { if strongSelf.replyBackgroundNode == nil { @@ -320,37 +323,37 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { 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 { - self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + case .tap: + if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { + if let item = self.item, let author = item.message.author { + self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + } + return } - return - } - - if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { - if let item = self.item { - for attribute in item.message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) - return + + if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + return + } } } } - } - - if let item = self.item, self.videoNode.frame.contains(location) { - self.controllerInteraction?.openMessage(item.message.id) - return - } - - self.controllerInteraction?.clickThroughMessage() - case .longTap, .doubleTap: - if let item = self.item, self.videoNode.frame.contains(location) { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.videoNode.frame) - } - case .hold: - break + + if let item = self.item, let hostedVideoNode = self.hostedVideoNode, hostedVideoNode.frame.contains(location) { + self.controllerInteraction?.openMessage(item.message.id) + return + } + + self.controllerInteraction?.clickThroughMessage() + case .longTap, .doubleTap: + if let item = self.item, let hostedVideoNode = self.hostedVideoNode, hostedVideoNode.frame.contains(location) { + self.controllerInteraction?.openMessageContextMenu(item.message.id, self, hostedVideoNode.frame) + } + case .hold: + break } } default: diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index f961a08cfb..d6a276002c 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -149,6 +149,8 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } else { viewClassName = ChatMessageActionItemNode.self } + } else if let _ = media as? TelegramMediaExpiredContent { + viewClassName = ChatMessageActionItemNode.self } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index b916fd4c5f..6e88241d93 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -27,6 +27,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { super.init() self.addSubnode(self.contentNode) + self.contentNode.openMedia = { [weak self] in + if let strongSelf = self, let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction { + controllerInteraction.openMessage(item.message.id) + } + } } required init?(coder aDecoder: NSCoder) { @@ -68,9 +73,13 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } if let file = webpage.file { - mediaAndFlags = (file, []) + if let image = webpage.image, let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { + mediaAndFlags = (image, []) + } else { + mediaAndFlags = (file, []) + } } else if let image = webpage.image { - if let type = webpage.type, ["photo"].contains(type) { + if let type = webpage.type, ["photo", "video", "embed"].contains(type) { var flags = ChatMessageAttachedContentNodeMediaFlags() if webpage.instantPage != nil { flags.insert(.preferMediaBeforeText) @@ -92,6 +101,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return (size, { [weak self] animation in if let strongSelf = self { + strongSelf.item = item strongSelf.webPage = webPage apply(animation) @@ -132,10 +142,42 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func updateHiddenMedia(_ media: [Media]?) { - self.contentNode.updateHiddenMedia(media) + if let media = media { + var updatedMedia: [Media]? + for item in media { + if let webpage = item as? TelegramMediaWebpage, let current = self.webPage, webpage.isEqual(current) { + var mediaList: [Media] = [webpage] + if case let .Loaded(content) = webpage.content { + if let image = content.image { + mediaList.append(image) + } + if let file = content.file { + mediaList.append(file) + } + } + updatedMedia = mediaList + } + } + self.contentNode.updateHiddenMedia(updatedMedia) + } else { + self.contentNode.updateHiddenMedia(nil) + } } override func transitionNode(media: Media) -> ASDisplayNode? { - return self.contentNode.transitionNode(media: media) + if let result = self.contentNode.transitionNode(media: media) { + return result + } + if let webpage = media as? TelegramMediaWebpage, let current = self.webPage, webpage.isEqual(current) { + if case let .Loaded(content) = webpage.content { + if let image = content.image, let result = self.contentNode.transitionNode(media: image) { + return result + } + if let file = content.file, let result = self.contentNode.transitionNode(media: file) { + return result + } + } + } + return nil } } diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index 53ca90329b..d3a2e3e82f 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -88,7 +88,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - let panelHeight: CGFloat = 44.0 + let panelHeight: CGFloat = 50.0 if self.theme !== interfaceState.theme { self.theme = interfaceState.theme @@ -116,10 +116,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let leftInset: CGFloat = 10.0 let rightInset: CGFloat = 18.0 - transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 5.0), size: CGSize(width: 2.0, height: panelHeight - 10.0))) + transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: leftInset, 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: 16.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - rightInset - closeButtonSize.width - 4.0, height: panelHeight)) @@ -154,9 +154,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let _ = titleApply() let _ = textApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 5.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 8.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: leftInset + textLineInset, y: 26.0), size: textLayout.size) } } } diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index 22986a4780..42630a0ff3 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -25,11 +25,11 @@ class ChatVideoGalleryItem: GalleryItem { for media in self.message.media { if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) + node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) break } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let file = content.file, (file.isVideo || file.mimeType.hasPrefix("video/")) { - node.setFile(account: account, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) + node.setFile(account: account, stableId: self.message.stableId, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia) break } } @@ -51,14 +51,15 @@ class ChatVideoGalleryItem: GalleryItem { } } +private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white) + final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _ready = Promise() fileprivate let _title = Promise() fileprivate let _titleView = Promise() + fileprivate let _rightBarButtonItem = Promise() - private var player: MediaPlayer? - private let snapshotNode: TransformImageNode - private let videoNode: MediaPlayerNode + private var videoNode: TelegramVideoNode? private let scrubberView: ChatVideoGalleryItemScrubberView private let progressButtonNode: HighlightableButtonNode @@ -76,10 +77,6 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let footerContentNode: ChatItemGalleryFooterContentNode init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { - self.videoNode = MediaPlayerNode() - self.snapshotNode = TransformImageNode() - self.snapshotNode.backgroundColor = UIColor.black - self.videoNode.snapshotNode = snapshotNode self.scrubberView = ChatVideoGalleryItemScrubberView() self.progressButtonNode = HighlightableButtonNode() @@ -89,13 +86,9 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { super.init() - self.snapshotNode.imageUpdated = { [weak self] in - self?._ready.set(.single(Void())) - } - self._titleView.set(.single(self.scrubberView)) self.scrubberView.seek = { [weak self] timestamp in - self?.player?.seek(timestamp: timestamp) + self?.videoNode?.seek(timestamp) } self.progressButtonNode.addSubnode(self.progressNode) @@ -122,16 +115,36 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate func setMessage(_ message: Message) { self.footerContentNode.setMessage(message) + + self.message = message + + var rightBarButtonItem: UIBarButtonItem? + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isVideo { + rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) + break + } + } + } + self._rightBarButtonItem.set(.single(rightBarButtonItem)) } - func setFile(account: Account, file: TelegramMediaFile, loopVideo: Bool) { + func setFile(account: Account, stableId: UInt32, file: TelegramMediaFile, loopVideo: Bool) { if self.accountAndFile == nil || !self.accountAndFile!.1.isEqual(file) || !self.accountAndFile!.2 != loopVideo { + if let videoNode = self.videoNode { + videoNode.pause() + videoNode.removeFromSupernode() + self.videoNode = nil + } if let largestSize = file.dimensions { - self.snapshotNode.alphaTransitionOnFirstUpdate = false - let displaySize = largestSize.dividedByScreenScale() - self.snapshotNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.snapshotNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) - self.zoomableContent = (largestSize, self.videoNode) + let videoNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .messageMedia(stableId: stableId, file: file), priority: 0, withSound: true) + videoNode.setShouldAcquireContext(true) + self.videoNode = videoNode + self.scrubberView.setStatusSignal(videoNode.status) + self.zoomableContent = (largestSize, videoNode) + + self._ready.set(.single(Void())) } else { self._ready.set(.single(Void())) } @@ -146,7 +159,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.progressButtonNode.isHidden = false case .Local: strongSelf.progressNode.state = .Play - strongSelf.progressButtonNode.isHidden = strongSelf.player != nil + strongSelf.progressButtonNode.isHidden = strongSelf.videoNode != nil case .Remote: strongSelf.progressNode.state = .Remote strongSelf.progressButtonNode.isHidden = false @@ -161,43 +174,60 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.accountAndFile = (account, file, loopVideo) if shouldPlayVideo && self.isCentral { self.progressButtonPressed() - //self.playVideo() } } } private func playVideo() { - if let (account, file, loopVideo) = self.accountAndFile { - var dimensions: CGSize? = file.dimensions - if dimensions == nil || dimensions!.width.isLessThanOrEqualTo(0.0) || dimensions!.height.isLessThanOrEqualTo(0.0) { - dimensions = largestImageRepresentation(file.previewRepresentations)?.dimensions.aspectFitted(CGSize(width: 1920, height: 1080)) - } - if dimensions == nil || dimensions!.width.isLessThanOrEqualTo(0.0) || dimensions!.height.isLessThanOrEqualTo(0.0) { - dimensions = CGSize(width: 1920, height: 1080) - } - - if let dimensions = dimensions, !dimensions.width.isLessThanOrEqualTo(0.0) && !dimensions.height.isLessThanOrEqualTo(0.0) { - /*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) - self.videoNode.player = VideoPlayer(source: source)*/ - - let player = MediaPlayer(audioSessionManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, postbox: account.postbox, resource: file.resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: true) - if loopVideo { - player.actionAtEnd = .loop + if let videoNode = self.videoNode { + videoNode.play() + } else { + if let (account, file, loop) = self.accountAndFile, let message = self.message { + if let largestSize = file.dimensions { + let videoNode = TelegramVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .messageMedia(stableId: message.stableId, file: file), priority: 0, withSound: true) + videoNode.setShouldAcquireContext(true) + self.scrubberView.setStatusSignal(videoNode.status) + self.videoNode = videoNode + self.zoomableContent = (largestSize, videoNode) + + self._ready.set(.single(Void())) + } else { + self.scrubberView.setStatusSignal(nil) + self._ready.set(.single(Void())) } - player.attachPlayerNode(self.videoNode) - self.progressButtonNode.isHidden = true - self.player = player - self.scrubberView.setStatusSignal(player.status) - player.play() - self.zoomableContent = (dimensions, self.videoNode) + self.resourceStatus = nil + self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.resourceStatus = status + switch status { + case let .Fetching(progress): + strongSelf.progressNode.state = .Fetching(progress: progress) + strongSelf.progressButtonNode.isHidden = false + case .Local: + strongSelf.progressNode.state = .Play + strongSelf.progressButtonNode.isHidden = strongSelf.videoNode != nil + case .Remote: + strongSelf.progressNode.state = .Remote + strongSelf.progressButtonNode.isHidden = false + } + } + })) + if self.progressButtonNode.supernode == nil { + self.addSubnode(self.progressButtonNode) + } } } } private func stopVideo() { - self.player = nil - self.progressButtonNode.isHidden = false + if let videoNode = self.videoNode { + videoNode.pause() + self.progressButtonNode.isHidden = false + + self.videoNode = nil + self.zoomableContent = nil + } } override func centralityUpdated(isCentral: Bool) { @@ -214,22 +244,45 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func animateIn(from node: ASDisplayNode) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.videoNode.view.superview) + guard let videoNode = self.videoNode else { + return + } - self.videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(self.videoNode.layer.transform, transformedFrame.size.width / self.videoNode.layer.bounds.size.width, transformedFrame.size.height / self.videoNode.layer.bounds.size.height, 1.0) - self.videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + if let node = node as? TelegramVideoNode, let account = self.accountAndFile?.0 { + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) + } else { + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + } } override func animateOut(to node: ASDisplayNode, completion: @escaping () -> Void) { - var transformedFrame = node.view.convert(node.view.bounds, to: self.videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.videoNode.view.superview) + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = self.videoNode.view.convert(self.videoNode.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) var positionCompleted = false var boundsCompleted = false @@ -256,12 +309,12 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { intermediateCompletion() }) - self.videoNode.layer.animatePosition(from: self.videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in positionCompleted = true intermediateCompletion() }) - self.videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in //positionCompleted = true @@ -270,19 +323,86 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - self.videoNode.snapshotNode?.isHidden = true - transformedFrame.origin = CGPoint() - let transform = CATransform3DScale(self.videoNode.layer.transform, transformedFrame.size.width / self.videoNode.layer.bounds.size.width, transformedFrame.size.height / self.videoNode.layer.bounds.size.height, 1.0) - self.videoNode.layer.animate(from: NSValue(caTransform3D: self.videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in boundsCompleted = true intermediateCompletion() }) } + func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + var nodeCompleted = false + + let copyView = node.view.snapshotContentTree()! + + //self.view.insertSubview(copyView, belowSubview: self.scrollView) + videoNode.isHidden = true + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + + //node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) + node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + nodeCompleted = true + intermediateCompletion() + }) + } + override func title() -> Signal { - //return self._title.get() return .single("") } @@ -290,6 +410,10 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { return self._titleView.get() } + override func rightBarButtonItem() -> Signal { + return self._rightBarButtonItem.get() + } + private func activateVideo() { if let (account, file, _) = self.accountAndFile { if let resourceStatus = self.resourceStatus { @@ -320,6 +444,44 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + @objc func pictureInPictureButtonPressed() { + 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 { return .single(self.footerContentNode) } diff --git a/TelegramUI/ChatVideoGalleryItemScrubberView.swift b/TelegramUI/ChatVideoGalleryItemScrubberView.swift index bb26c89f21..420d938f7a 100644 --- a/TelegramUI/ChatVideoGalleryItemScrubberView.swift +++ b/TelegramUI/ChatVideoGalleryItemScrubberView.swift @@ -7,10 +7,32 @@ final class ChatVideoGalleryItemScrubberView: UIView { private let rightTimestampNode: MediaPlayerTimeTextNode private let scrubberNode: MediaPlayerScrubbingNode + private var playbackStatus: MediaPlayerStatus? + + var hideWhenDurationIsUnknown = false { + didSet { + if self.hideWhenDurationIsUnknown { + if let playbackStatus = self.playbackStatus, !playbackStatus.duration.isZero { + self.scrubberNode.isHidden = false + self.leftTimestampNode.isHidden = false + self.rightTimestampNode.isHidden = false + } else { + self.scrubberNode.isHidden = true + self.leftTimestampNode.isHidden = true + self.rightTimestampNode.isHidden = true + } + } else { + self.scrubberNode.isHidden = false + self.leftTimestampNode.isHidden = false + self.rightTimestampNode.isHidden = false + } + } + } + var seek: (Double) -> Void = { _ in } override init(frame: CGRect) { - self.scrubberNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: .gray, foregroundColor: .white) + self.scrubberNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white) self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white) @@ -23,6 +45,27 @@ final class ChatVideoGalleryItemScrubberView: UIView { self?.seek(timestamp) } + self.scrubberNode.playerStatusUpdated = { [weak self] status in + if let strongSelf = self { + strongSelf.playbackStatus = status + if strongSelf.hideWhenDurationIsUnknown { + if let playbackStatus = status, !playbackStatus.duration.isZero { + strongSelf.scrubberNode.isHidden = false + strongSelf.leftTimestampNode.isHidden = false + strongSelf.rightTimestampNode.isHidden = false + } else { + strongSelf.scrubberNode.isHidden = true + strongSelf.leftTimestampNode.isHidden = true + strongSelf.rightTimestampNode.isHidden = true + } + } else { + strongSelf.scrubberNode.isHidden = false + strongSelf.leftTimestampNode.isHidden = false + strongSelf.rightTimestampNode.isHidden = false + } + } + } + self.addSubnode(self.scrubberNode) self.addSubnode(self.leftTimestampNode) self.addSubnode(self.rightTimestampNode) diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index 43fb8f9fd0..f68c0a7bcd 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -127,7 +127,7 @@ public class ComposeController: ViewController { }, error: { _ in if let controller = controller { controller.displayNavigationActivity = false - controller.present(standardTextAlertController(title: nil, text: "An error occurred.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + controller.present(standardTextAlertController(title: nil, text: "An error occurred.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } })) } diff --git a/TelegramUI/CurrencyFormat.swift b/TelegramUI/CurrencyFormat.swift new file mode 100644 index 0000000000..18b6176dbd --- /dev/null +++ b/TelegramUI/CurrencyFormat.swift @@ -0,0 +1,90 @@ +import Foundation + +private final class CurrencyFormatterEntry { + let symbol: String + let thousandsSeparator: String + let decimalSeparator: String + let symbolOnLeft: Bool + let spaceBetweenAmountAndSymbol: Bool + let decimalDigits: Int + + init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + self.symbol = symbol + self.thousandsSeparator = thousandsSeparator + self.decimalSeparator = decimalSeparator + self.symbolOnLeft = symbolOnLeft + self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol + self.decimalDigits = decimalDigits + } +} + +private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] { + guard let filePath = frameworkBundle.path(forResource: "currencies", ofType: "json") else { + return [:] + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [:] + } + + guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else { + return [:] + } + + var result: [String: CurrencyFormatterEntry] = [:] + + for (code, contents) in dict { + if let contentsDict = contents as? [String: AnyObject] { + let entry = CurrencyFormatterEntry(symbol: contentsDict["symbol"] as! String, thousandsSeparator: contentsDict["thousandsSeparator"] as! String, decimalSeparator: contentsDict["decimalSeparator"] as! String, symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue, spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue, decimalDigits: (contentsDict["decimalDigits"] as! NSNumber).intValue) + result[code] = entry + result[code.lowercased()] = entry + } + } + + return result +} + +private let currencyFormatterEntries = loadCurrencyFormatterEntries() + +public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { + if let entry = currencyFormatterEntries[currency] { + var result = "" + if amount < 0 { + result.append("-") + } + if entry.symbolOnLeft { + result.append(entry.symbol) + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + } + var integerPart = abs(amount) + var fractional: [Character] = [] + for _ in 0 ..< entry.decimalDigits { + let part = integerPart % 10 + integerPart /= 10 + if let scalar = UnicodeScalar(UInt32(part + 48)) { + fractional.append(Character(scalar)) + } + } + result.append("\(integerPart)") + result.append(entry.decimalSeparator) + for i in 0 ..< fractional.count { + result.append(fractional[fractional.count - i - 1]) + } + if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + result.append(entry.symbol) + } + + return result + } else { + assertionFailure() + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currency + formatter.negativeFormat = "-¤#,##0.00" + return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? "" + } +} diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift index da67d2ef65..9c275ca54f 100644 --- a/TelegramUI/DebugAccountsController.swift +++ b/TelegramUI/DebugAccountsController.swift @@ -121,7 +121,7 @@ public func debugAccountsController(account: Account, accountManager: AccountMan let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window, with: a) + controller?.present(c, in: .window(.root), with: a) } return controller } diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index e41e926cc4..17678bb548 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -20,11 +20,13 @@ private final class DebugControllerArguments { private enum DebugControllerSection: Int32 { case logs + case payments } private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case accounts(PresentationTheme) + case clearPaymentData(PresentationTheme) var section: ItemListSectionId { switch self { @@ -32,6 +34,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logs.rawValue case .accounts: return DebugControllerSection.logs.rawValue + case .clearPaymentData: + return DebugControllerSection.payments.rawValue } } @@ -41,6 +45,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 0 case .accounts: return 1 + case .clearPaymentData: + return 2 } } @@ -58,6 +64,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .clearPaymentData(lhsTheme): + if case let .clearPaymentData(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } } } @@ -91,6 +103,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { arguments.pushController(debugAccountsController(account: arguments.account, accountManager: arguments.accountManager)) }) + case let .clearPaymentData(theme): + return ItemListDisclosureItem(theme: theme, title: "Clear Payment Data", label: "", sectionId: self.section, style: .blocks, action: { + let _ = cacheTwoStepPasswordToken(postbox: arguments.account.postbox, token: nil).start() + }) } } } @@ -100,6 +116,7 @@ private func debugControllerEntries(presentationData: PresentationData) -> [Debu entries.append(.sendLogs(presentationData.theme)) entries.append(.accounts(presentationData.theme)) + entries.append(.clearPaymentData(presentationData.theme)) return entries } @@ -124,7 +141,7 @@ public func debugController(account: Account, accountManager: AccountManager) -> let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window, with: a) + controller?.present(c, in: .window(.root), with: a) } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 3fee9e5e00..5504d79ee8 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -11,6 +11,7 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(GeneratedMediaStoreSettings.self, f: { GeneratedMediaStoreSettings(decoder: $0) }) declareEncodable(PresentationThemeSettings.self, f: { PresentationThemeSettings(decoder: $0) }) declareEncodable(TelegramWallpaper.self, f: { TelegramWallpaper(decoder: $0) }) + declareEncodable(ApplicationSpecificBoolNotice.self, f: { ApplicationSpecificBoolNotice(decoder: $0) }) return }() diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 63a07ce59f..733956a613 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -133,7 +133,7 @@ private let bubble = PresentationThemeChatBubble( shareButtonFillColor: UIColor(rgb: 0x748391, alpha: 0.45), shareButtonForegroundColor: .white, mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), - mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 0.6), + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), actionButtonsFillColor: UIColor(rgb: 0x596E89), actionButtonsTextColor: .white ) diff --git a/TelegramUI/DeviceContactsManager.swift b/TelegramUI/DeviceContactsManager.swift new file mode 100644 index 0000000000..31d1ed66de --- /dev/null +++ b/TelegramUI/DeviceContactsManager.swift @@ -0,0 +1,90 @@ +import Foundation +import Contacts +import SwiftSignalKit +import Postbox +import TelegramCore + +private func authorizedContacts() -> Signal { + return Signal { subscriber in + if #available(iOSApplicationExtension 9.0, *) { + if CNContactStore.authorizationStatus(for: .contacts) == .notDetermined { + let store = CNContactStore() + store.requestAccess(for: .contacts, completionHandler: { authorized, _ in + subscriber.putNext(authorized) + subscriber.putCompletion() + }) + } else if CNContactStore.authorizationStatus(for: .contacts) == .authorized { + subscriber.putNext(true) + subscriber.putCompletion() + } + } else { + + } + + return EmptyDisposable + } +} + +@available(iOSApplicationExtension 9.0, *) +private func retrieveContactsWithStore(_ store: CNContactStore) -> [DeviceContact] { + let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor] + + let request = CNContactFetchRequest(keysToFetch: keysToFetch) + request.unifyResults = true + + var result: [DeviceContact] = [] + let _ = try? store.enumerateContacts(with: request, usingBlock: { contact, _ in + var phoneNumbers: [DeviceContactPhoneNumber] = [] + for number in contact.phoneNumbers { + phoneNumbers.append(DeviceContactPhoneNumber(label: number.label ?? "", number: number.value.stringValue)) + } + result.append(DeviceContact(id: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)) + }) + return result +} + +@available(iOSApplicationExtension 9.0, *) +private func modernContacts() -> Signal<[DeviceContact], NoError> { + return authorizedContacts() + |> mapToSignal { authorized -> Signal<[DeviceContact], NoError> in + return Signal { subscriber in + let queue = Queue() + let disposable = MetaDisposable() + queue.async { + let store = CNContactStore() + var current = retrieveContactsWithStore(store) + subscriber.putNext(current) + + let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { _ in + queue.async { + let updated = retrieveContactsWithStore(store) + if current != updated { + current = updated + subscriber.putNext(updated) + } + } + }) + + disposable.set(ActionDisposable { + NotificationCenter.default.removeObserver(handle) + }) + } + return disposable + } + } +} + +public final class DeviceContactsManager { + private let contactsValue = Promise<[DeviceContact]>() + public var contacts: Signal<[DeviceContact], NoError> { + return self.contactsValue.get() + } + + init() { + if #available(iOSApplicationExtension 9.0, *) { + self.contactsValue.set(modernContacts()) + } else { + + } + } +} diff --git a/TelegramUI/EmbedGalleryVideoItem.swift b/TelegramUI/EmbedGalleryVideoItem.swift new file mode 100644 index 0000000000..7da87c266a --- /dev/null +++ b/TelegramUI/EmbedGalleryVideoItem.swift @@ -0,0 +1,421 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class EmbedVideoGalleryItem: GalleryItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let message: Message + let location: MessageHistoryEntryLocation? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { + self.account = account + self.theme = theme + self.strings = strings + self.message = message + self.location = location + } + + func node() -> GalleryItemNode { + let node = EmbedVideoGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) + + for media in self.message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + node.setWebpage(account: account, webpage: content) + break + } + } + + if let location = self.location { + node._title.set(.single("\(location.index + 1) of \(location.count)")) + } + node.setMessage(self.message) + + return node + } + + func updateNode(node: GalleryItemNode) { + if let node = node as? EmbedVideoGalleryItemNode, let location = self.location { + node._title.set(.single("\(location.index + 1) of \(location.count)")) + node.setMessage(self.message) + } + } +} + +private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white) + +final class EmbedVideoGalleryItemNode: ZoomableContentGalleryItemNode { + fileprivate let _ready = Promise() + fileprivate let _title = Promise() + fileprivate let _titleView = Promise() + fileprivate let _rightBarButtonItem = Promise() + + private var videoNode: EmbedVideoNode? + private let scrubberView: ChatVideoGalleryItemScrubberView + + private let progressButtonNode: HighlightableButtonNode + private let progressNode: RadialProgressNode + + private var accountAndWebpage: (Account, TelegramMediaWebpageLoadedContent)? + private var message: Message? + + private var isCentral = false + + private let fetchStatusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var resourceStatus: MediaResourceStatus? + + private let footerContentNode: ChatItemGalleryFooterContentNode + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.scrubberView = ChatVideoGalleryItemScrubberView() + self.scrubberView.hideWhenDurationIsUnknown = true + + self.progressButtonNode = HighlightableButtonNode() + self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) + + self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) + + super.init() + + self._titleView.set(.single(self.scrubberView)) + self.scrubberView.seek = { [weak self] timestamp in + self?.videoNode?.seek(timestamp) + } + + self.progressButtonNode.addSubnode(self.progressNode) + self.progressButtonNode.addTarget(self, action: #selector(progressButtonPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.fetchStatusDisposable.dispose() + self.fetchDisposable.dispose() + } + + override func ready() -> Signal { + return self._ready.get() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let progressDiameter: CGFloat = 50.0 + let progressFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressDiameter) / 2.0), y: floor((layout.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) + transition.updateFrame(node: self.progressButtonNode, frame: progressFrame) + transition.updateFrame(node: self.progressNode, frame: CGRect(origin: CGPoint(), size: progressFrame.size)) + } + + fileprivate func setMessage(_ message: Message) { + self.footerContentNode.setMessage(message) + + self.message = message + + var rightBarButtonItem: UIBarButtonItem? + for media in message.media { + if let _ = media as? TelegramMediaWebpage { + rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed)) + break + } + } + self._rightBarButtonItem.set(.single(rightBarButtonItem)) + } + + func setWebpage(account: Account, webpage: TelegramMediaWebpageLoadedContent) { + if self.accountAndWebpage == nil || self.accountAndWebpage!.1 != webpage { + if let videoNode = self.videoNode { + videoNode.pause() + videoNode.removeFromSupernode() + self.videoNode = nil + } + if let largestSize = webpage.embedSize { + let videoNode = EmbedVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .webpage(webpage), priority: 0, withSound: true) + videoNode.isUserInteractionEnabled = false + scrubberView.setStatusSignal(videoNode.status) + videoNode.setShouldAcquireContext(true) + self.videoNode = videoNode + self.zoomableContent = (largestSize, videoNode) + + self._ready.set(videoNode.ready) + } else { + self._ready.set(.single(Void())) + } + + /*self.resourceStatus = nil + self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.resourceStatus = status + switch status { + case let .Fetching(progress): + strongSelf.progressNode.state = .Fetching(progress: progress) + strongSelf.progressButtonNode.isHidden = false + case .Local: + strongSelf.progressNode.state = .Play + strongSelf.progressButtonNode.isHidden = strongSelf.player != nil + case .Remote: + strongSelf.progressNode.state = .Remote + strongSelf.progressButtonNode.isHidden = false + } + } + })) + if self.progressButtonNode.supernode == nil { + self.addSubnode(self.progressButtonNode) + }*/ + + self.accountAndWebpage = (account, webpage) + if true && self.isCentral { + self.progressButtonPressed() + } + } + } + + private func playVideo() { + self.videoNode?.play() + } + + private func stopVideo() { + self.videoNode?.pause() + self.progressButtonNode.isHidden = false + } + + override func centralityUpdated(isCentral: Bool) { + super.centralityUpdated(isCentral: isCentral) + + if self.isCentral != isCentral { + self.isCentral = isCentral + if isCentral { + self.playVideo() + } else { + self.stopVideo() + } + } + } + + override func animateIn(from node: ASDisplayNode) { + guard let videoNode = self.videoNode else { + return + } + + if let node = node as? EmbedVideoNode, let account = self.accountAndWebpage?.0 { + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + account.telegramApplicationContext.mediaManager.setOverlayVideoNode(nil) + } else { + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + } + } + + override func animateOut(to node: ASDisplayNode, completion: @escaping () -> Void) { + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + + let copyView = node.view.snapshotContentTree()! + + self.view.insertSubview(copyView, belowSubview: self.scrollView) + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + } + + func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + var nodeCompleted = false + + let copyView = node.view.snapshotContentTree()! + + //self.view.insertSubview(copyView, belowSubview: self.scrollView) + videoNode.isHidden = true + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + + //node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) + node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + nodeCompleted = true + intermediateCompletion() + }) + } + + override func title() -> Signal { + return .single("") + } + + override func titleView() -> Signal { + return self._titleView.get() + } + + override func rightBarButtonItem() -> Signal { + return self._rightBarButtonItem.get() + } + + private func activateVideo() { + if let _ = self.accountAndWebpage { + self.playVideo() + } + } + + @objc func progressButtonPressed() { + if let _ = self.accountAndWebpage { + self.playVideo() + } + } + + @objc func pictureInPictureButtonPressed() { + if let account = self.accountAndWebpage?.0, let message = self.message, let webpage = self.accountAndWebpage?.1 { + let overlayNode = EmbedVideoNode(manager: account.telegramApplicationContext.mediaManager, account: account, source: .webpage(webpage), 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 { + return .single(self.footerContentNode) + } +} diff --git a/TelegramUI/EmbedVideoNode.swift b/TelegramUI/EmbedVideoNode.swift new file mode 100644 index 0000000000..04818c2a69 --- /dev/null +++ b/TelegramUI/EmbedVideoNode.swift @@ -0,0 +1,601 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +import TelegramLegacyComponents + +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 thumbnail = Promise() + private var thumbnailDisposable: 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() + + if let image = webpage.image { + self.thumbnailDisposable = (rawMessagePhoto(account: account, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in + if let strongSelf = self { + strongSelf.thumbnail.set(.single(image)) + strongSelf._ready.set(.single()) + } + }) + } else { + self._ready.set(.single()) + } + + self.playerView.requestAudioSession = { [weak self] in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self, !strongSelf.hasAudioSession { + strongSelf.audioSessionDisposable.set(audioSessionManager.push(audioSessionType: .play, overrideSpeaker: false, once: false, activate: { + if let strongSelf = self { + strongSelf.hasAudioSession = true + } + }, deactivate: { + return Signal { subscriber in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.hasAudioSession { + strongSelf.hasAudioSession = false + strongSelf.audioSessionDisposable.set(nil) + strongSelf.playerView.pauseVideo() + } + subscriber.putCompletion() + } + + return EmptyDisposable + } + })) + } + } + + self.playerView.disposeAudioSession = { [weak self] in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self { + strongSelf.hasAudioSession = false + strongSelf.audioSessionDisposable.set(nil) + } + } + + self.playerView.stateSignal() + } + + deinit { + audioSessionDisposable.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 let progressNode: RadialProgressNode + private let controlsNode: PictureInPictureVideoControlsNode? + private var minimizedBlurView: UIVisualEffectView? + private var minimizedArrowView: TGEmbedPIPPullArrowView? + private var minimizedEdge: OverlayMediaItemMinimizationEdge? + + private var statusDisposable: 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: 0) + } + + 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() + 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 { + controlsNode.status = self.status + self.addSubnode(controlsNode) + } + + if let image = source.image { + self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, 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) + } + } + } + + 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: 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? 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(whilePlaying: next.playing) + } else { + status = .paused + } + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, status: status)) + } + }) + return ActionDisposable { + innerDisposable?.dispose() + } + }) + self._ready.set(context.ready) + } + }) + } + } + + 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/EmojiUtils.swift b/TelegramUI/EmojiUtils.swift index cb30a3e03e..5fb871cb27 100644 --- a/TelegramUI/EmojiUtils.swift +++ b/TelegramUI/EmojiUtils.swift @@ -20,46 +20,37 @@ extension UnicodeScalar { } extension String { - var glyphCount: Int { - let richText = NSAttributedString(string: self) let line = CTLineCreateWithAttributedString(richText) return CTLineGetGlyphCount(line) } var isSingleEmoji: Bool { - return glyphCount == 1 && containsEmoji } var containsEmoji: Bool { - return !unicodeScalars.filter { $0.isEmoji }.isEmpty } var containsOnlyEmoji: Bool { - return unicodeScalars.first(where: { !$0.isEmoji && !$0.isZeroWidthJoiner }) == nil } // The next tricks are mostly to demonstrate how tricky it can be to determine emoji's // If anyone has suggestions how to improve this, please let me know var emojiString: String { - return emojiScalars.map { String($0) }.reduce("", +) } var emojis: [String] { - var scalars: [[UnicodeScalar]] = [] var currentScalarSet: [UnicodeScalar] = [] var previousScalar: UnicodeScalar? for scalar in emojiScalars { - if let prev = previousScalar, !prev.isZeroWidthJoiner && !scalar.isZeroWidthJoiner { - scalars.append(currentScalarSet) currentScalarSet = [] } @@ -74,15 +65,12 @@ extension String { } fileprivate var emojiScalars: [UnicodeScalar] { - var chars: [UnicodeScalar] = [] var previous: UnicodeScalar? for cur in unicodeScalars { - if let previous = previous, previous.isZeroWidthJoiner && cur.isEmoji { chars.append(previous) chars.append(cur) - } else if cur.isEmoji { chars.append(cur) } diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index 6b9029bacf..1ecc02ec5e 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -232,7 +232,7 @@ public func featuredStickerPacksController(account: Account) -> ViewController { presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } diff --git a/TelegramUI/FrameworkBundle.swift b/TelegramUI/FrameworkBundle.swift index adc1f674ee..d4261b2d33 100644 --- a/TelegramUI/FrameworkBundle.swift +++ b/TelegramUI/FrameworkBundle.swift @@ -11,3 +11,4 @@ extension UIImage { self.init(named: bundleImageName, in: frameworkBundle, compatibleWith: nil) } } + diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index d1c0ee4fb8..fa704d6ed2 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -52,7 +52,9 @@ private func mediaForMessage(message: Message) -> Media? { } else if let webpage = media as? TelegramMediaWebpage { switch webpage.content { case let .Loaded(content): - if let image = content.image { + if let embedUrl = content.embedUrl, !embedUrl.isEmpty { + return webpage + } else if let image = content.image { if let result = galleryMediaForMedia(media: image) { return result } @@ -85,6 +87,8 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr return ChatDocumentGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + return EmbedVideoGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } } default: @@ -151,6 +155,7 @@ class GalleryController: ViewController { private let centralItemTitle = Promise() private let centralItemTitleView = Promise() + private let centralItemRightBarButtonItem = Promise() private let centralItemNavigationStyle = Promise() private let centralItemFooterContentNode = Promise() private let centralItemAttributesDisposable = DisposableSet(); @@ -161,10 +166,12 @@ class GalleryController: ViewController { } private let replaceRootController: (ViewController, ValuePromise?) -> Void + private let baseNavigationController: NavigationController? - init(account: Account, messageId: MessageId, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { + init(account: Account, messageId: MessageId, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { self.account = account self.replaceRootController = replaceRootController + self.baseNavigationController = baseNavigationController self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -227,6 +234,10 @@ class GalleryController: ViewController { self?.navigationItem.titleView = titleView })) + self.centralItemAttributesDisposable.add(self.centralItemRightBarButtonItem.get().start(next: { [weak self] rightBarButtonItem in + self?.navigationItem.rightBarButtonItem = rightBarButtonItem + })) + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) @@ -296,7 +307,7 @@ class GalleryController: ViewController { override func loadDisplayNode() { let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in if let strongSelf = self { - strongSelf.present(controller, in: .window, with: arguments) + strongSelf.present(controller, in: .window(.root), with: arguments) } }, dismissController: { [weak self] in self?.dismiss(forceAway: true) @@ -328,6 +339,48 @@ class GalleryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) } + self.galleryNode.beginCustomDismiss = { [weak self] in + if let strongSelf = self { + strongSelf._hiddenMedia.set(.single(nil)) + + var animatedOutNode = true + var animatedOutInterface = false + + let completion = { + if animatedOutNode && animatedOutInterface { + //self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + } + + /*if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { + if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { + if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: { + animatedOutNode = true + completion() + }) + } + } + }*/ + + strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + //completion() + }) + } + } + + self.galleryNode.completeCustomDismiss = { [weak self] in + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + let baseNavigationController = self.baseNavigationController + self.galleryNode.baseNavigationController = { [weak baseNavigationController] in + return baseNavigationController + } + self.galleryNode.pager.replaceItems(self.entries.map({ galleryItemForEntry(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: $0) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in @@ -341,6 +394,7 @@ class GalleryController: ViewController { if let node = strongSelf.galleryNode.pager.centralItemNode() { strongSelf.centralItemTitle.set(node.title()) strongSelf.centralItemTitleView.set(node.titleView()) + strongSelf.centralItemRightBarButtonItem.set(node.rightBarButtonItem()) strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } @@ -361,6 +415,7 @@ class GalleryController: ViewController { if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) + self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index eed8bf5a61..962695e0f1 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -1,6 +1,7 @@ import Foundation import AsyncDisplayKit import Display +import Postbox class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { var statusBar: StatusBar? @@ -15,6 +16,10 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { var scrollView: UIScrollView var pager: GalleryPagerNode + var beginCustomDismiss: () -> Void = { } + var completeCustomDismiss: () -> Void = { } + var baseNavigationController: () -> NavigationController? = { return nil } + private var presentationState = GalleryControllerPresentationState() var areControlsHidden = false @@ -49,6 +54,22 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + self.pager.beginCustomDismiss = { [weak self] in + if let strongSelf = self { + strongSelf.beginCustomDismiss() + } + } + + self.pager.completeCustomDismiss = { [weak self] in + if let strongSelf = self { + strongSelf.completeCustomDismiss() + } + } + + self.pager.baseNavigationController = { [weak self] in + return self?.baseNavigationController() + } + self.addSubnode(self.backgroundNode) self.scrollView.showsVerticalScrollIndicator = false diff --git a/TelegramUI/GalleryItemNode.swift b/TelegramUI/GalleryItemNode.swift index 3491006c31..92e7f590d9 100644 --- a/TelegramUI/GalleryItemNode.swift +++ b/TelegramUI/GalleryItemNode.swift @@ -2,6 +2,7 @@ import Foundation import AsyncDisplayKit import Display import SwiftSignalKit +import Postbox public enum GalleryItemNodeNavigationStyle { case light @@ -19,6 +20,9 @@ open class GalleryItemNode: ASDisplayNode { } var toggleControlsVisibility: () -> Void = { } + var beginCustomDismiss: () -> Void = { } + var completeCustomDismiss: () -> Void = { } + var baseNavigationController: () -> NavigationController? = { return nil } override init() { super.init(viewBlock: { @@ -38,6 +42,10 @@ open class GalleryItemNode: ASDisplayNode { return .single(nil) } + open func rightBarButtonItem() -> Signal { + return .single(nil) + } + open func footerContent() -> Signal { return .single(nil) } diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index f24192a674..f9795e44de 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -2,6 +2,7 @@ import Foundation import AsyncDisplayKit import Display import SwiftSignalKit +import Postbox final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private let pageGap: CGFloat @@ -23,6 +24,9 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { var centralItemIndexUpdated: (Int?) -> Void = { _ in } var toggleControlsVisibility: () -> Void = { } + var beginCustomDismiss: () -> Void = { } + var completeCustomDismiss: () -> Void = { } + var baseNavigationController: () -> NavigationController? = { return nil } init(pageGap: CGFloat) { self.pageGap = pageGap @@ -104,6 +108,9 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private func makeNodeForItem(at index: Int) -> GalleryItemNode { let node = self.items[index].node() node.toggleControlsVisibility = self.toggleControlsVisibility + node.beginCustomDismiss = self.beginCustomDismiss + node.completeCustomDismiss = self.completeCustomDismiss + node.baseNavigationController = self.baseNavigationController node.index = index return node } diff --git a/TelegramUI/GameController.swift b/TelegramUI/GameController.swift index c145679071..67d5f4e208 100644 --- a/TelegramUI/GameController.swift +++ b/TelegramUI/GameController.swift @@ -57,6 +57,10 @@ final class GameController: ViewController { fatalError("init(coder:) has not been implemented") } + deinit { + assert(true) + } + @objc func closePressed() { self.dismiss() } diff --git a/TelegramUI/GameControllerNode.swift b/TelegramUI/GameControllerNode.swift index 0a2d7e9857..eb1691300b 100644 --- a/TelegramUI/GameControllerNode.swift +++ b/TelegramUI/GameControllerNode.swift @@ -31,7 +31,7 @@ final class GameControllerNode: ViewControllerTracingNode { } func animateOut(completion: (() -> Void)? = nil) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in completion?() }) } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index ac3b38de7c..310224f7ae 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1073,7 +1073,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl result.set(true) }) ]) - contactsController.present(alertController, in: .window) + contactsController.present(alertController, in: .window(.root)) } return result.get() @@ -1292,7 +1292,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value, presentationArguments in - controller?.present(value, in: .window, with: presentationArguments) + controller?.present(value, in: .window(.root), with: presentationArguments) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { diff --git a/TelegramUI/ImageNode.swift b/TelegramUI/ImageNode.swift index b9474f201b..3b6c5b415b 100644 --- a/TelegramUI/ImageNode.swift +++ b/TelegramUI/ImageNode.swift @@ -121,6 +121,10 @@ public class ImageNode: ASDisplayNode { super.init() } + deinit { + self.disposable.dispose() + } + public func setSignal(_ signal: Signal) { var reportedHasImage = false self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 47694ae4bd..e46be5f960 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -446,7 +446,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } pushControllerImpl = { [weak controller] c in diff --git a/TelegramUI/InstantVideoNode.swift b/TelegramUI/InstantVideoNode.swift new file mode 100644 index 0000000000..6a5256b1b0 --- /dev/null +++ b/TelegramUI/InstantVideoNode.swift @@ -0,0 +1,347 @@ +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) + 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() + } 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 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 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, account: Account, source: InstantVideoNodeSource, priority: Int32, withSound: Bool) { + self.theme = theme + 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: 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(account: account, signal: chatMessageVideo(account: account, video: source.file)) + + self.statusDisposable = (chatMessageFileStatus(account: account, file: source.file) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + + } + }) + } + + 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 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.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/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 473a7f6a5f..744948202d 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -226,9 +226,9 @@ class ItemListActionItemNode: ListViewItemNode { switch item.alignment { case .natural: - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) case .center: - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: 12.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size) } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 20cdb70c97..c1b1de96a8 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -546,9 +546,12 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.statusNode.alpha = 0.0 strongSelf.nameNode.layer.animateAlpha(from: CGFloat(strongSelf.nameNode.layer.opacity), to: 0.0, duration: 0.3) strongSelf.nameNode.alpha = 0.0 + strongSelf.callButton.layer.animateAlpha(from: CGFloat(strongSelf.callButton.layer.opacity), to: 0.0, duration: 0.3) + strongSelf.callButton.alpha = 0.0 } else { strongSelf.statusNode.alpha = 0.0 strongSelf.nameNode.alpha = 0.0 + strongSelf.callButton.alpha = 0.0 } } else { var animateOut = false @@ -587,11 +590,16 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if animated && animateOut { strongSelf.statusNode.layer.animateAlpha(from: CGFloat(strongSelf.statusNode.layer.opacity), to: 1.0, duration: 0.3) strongSelf.statusNode.alpha = 1.0 + strongSelf.nameNode.layer.animateAlpha(from: CGFloat(strongSelf.nameNode.layer.opacity), to: 1.0, duration: 0.3) strongSelf.nameNode.alpha = 1.0 + + strongSelf.callButton.layer.animateAlpha(from: CGFloat(strongSelf.callButton.layer.opacity), to: 1.0, duration: 0.3) + strongSelf.callButton.alpha = 1.0 } else { strongSelf.statusNode.alpha = 1.0 strongSelf.nameNode.alpha = 1.0 + strongSelf.callButton.alpha = 1.0 } } if let presence = item.presence as? TelegramUserPresence { diff --git a/TelegramUI/ItemListCheckboxItem.swift b/TelegramUI/ItemListCheckboxItem.swift index 0f126d3618..96290ab96a 100644 --- a/TelegramUI/ItemListCheckboxItem.swift +++ b/TelegramUI/ItemListCheckboxItem.swift @@ -183,7 +183,7 @@ class ItemListCheckboxItemNode: ListViewItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 72d5990a39..d32eabfbd5 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -114,9 +114,17 @@ final class ItemListController: ViewController { return self._ready } + var enableInteractiveDismiss = false { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).enableInteractiveDismiss = self.enableInteractiveDismiss + } + } + } + var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? { didSet { - (self.displayNode as! ItemListNode).visibleEntriesUpdated = self.visibleEntriesUpdated + (self.displayNode as! ItemListControllerNode).visibleEntriesUpdated = self.visibleEntriesUpdated } } @@ -130,7 +138,7 @@ final class ItemListController: ViewController { self.statusBar.statusBarStyle = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme.rootController.statusBar.style.style self.scrollToTop = { [weak self] in - (self?.displayNode as! ItemListNode).scrollToTop() + (self?.displayNode as! ItemListControllerNode).scrollToTop() } if let tabBarItem = tabBarItem { @@ -250,19 +258,24 @@ final class ItemListController: ViewController { } } } |> map { ($0.theme, $1) } - let displayNode = ItemListNode(state: nodeState) + let displayNode = ItemListControllerNode(updateNavigationOffset: { [weak self] offset in + if let strongSelf = self { + strongSelf.navigationOffset = offset + } + }, state: nodeState) displayNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) } + displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss self.displayNode = displayNode super.displayNodeDidLoad() - self._ready.set((self.displayNode as! ItemListNode).ready) + self._ready.set((self.displayNode as! ItemListControllerNode).ready) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! ItemListNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @objc func leftNavigationButtonPressed() { @@ -276,23 +289,23 @@ final class ItemListController: ViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - (self.displayNode as! ItemListNode).listNode.preloadPages = true + (self.displayNode as! ItemListControllerNode).listNode.preloadPages = true if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { self.didPlayPresentationAnimation = true if case .modalSheet = presentationArguments.presentationAnimation { - (self.displayNode as! ItemListNode).animateIn() + (self.displayNode as! ItemListControllerNode).animateIn() } } } override func dismiss(completion: (() -> Void)? = nil) { - (self.displayNode as! ItemListNode).animateOut(completion: completion) + (self.displayNode as! ItemListControllerNode).animateOut(completion: completion) } func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? { var result: CGRect? - (self.displayNode as! ItemListNode).listNode.forEachItemNode { itemNode in + (self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListViewItemNode { if predicate(itemNode) { result = itemNode.convert(itemNode.bounds, to: self.displayNode) @@ -303,7 +316,7 @@ final class ItemListController: ViewController { } func forEachItemNode(_ f: (ListViewItemNode) -> Void) { - (self.displayNode as! ItemListNode).listNode.forEachItemNode { itemNode in + (self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListViewItemNode { f(itemNode) } diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 6b1f127a0d..54ebbc4043 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -84,7 +84,7 @@ final class ItemListNodeVisibleEntries: Sequence { } } -final class ItemListNode: ASDisplayNode { +class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { private var _ready = ValuePromise() public var ready: Signal { return self._ready.get() @@ -94,6 +94,7 @@ final class ItemListNode: ASDisplayNode { let listNode: ListView private var emptyStateItem: ItemListControllerEmptyStateItem? private var emptyStateNode: ItemListControllerEmptyStateItemNode? + private let scrollNode: ASScrollNode private let transitionDisposable = MetaDisposable() @@ -103,20 +104,47 @@ final class ItemListNode: ASDisplayNode { private var theme: PresentationTheme? private var listStyle: ItemListStyle? + let updateNavigationOffset: (CGFloat) -> Void var dismiss: (() -> Void)? var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? - init(state: Signal<(PresentationTheme, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { + var enableInteractiveDismiss = false { + didSet { + self.scrollNode.view.isScrollEnabled = self.enableInteractiveDismiss + } + } + + init(updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(PresentationTheme, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { + self.updateNavigationOffset = updateNavigationOffset + self.listNode = ListView() + self.scrollNode = ASScrollNode() super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.addSubnode(self.listNode) + self.backgroundColor = nil + self.isOpaque = false - self.backgroundColor = UIColor(rgb: 0xefeff4) + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.alwaysBounceHorizontal = false + self.scrollNode.view.alwaysBounceVertical = false + self.scrollNode.view.clipsToBounds = false + self.scrollNode.view.delegate = self + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.isScrollEnabled = false + self.addSubnode(self.scrollNode) + + self.scrollNode.backgroundColor = nil + self.scrollNode.isOpaque = false + + self.scrollNode.addSubnode(self.listNode) + self.addSubnode(self.scrollNode) + + self.listNode.backgroundColor = UIColor(rgb: 0xefeff4) self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let visibleEntriesUpdated = strongSelf.visibleEntriesUpdated, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { @@ -173,6 +201,18 @@ final class ItemListNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let previousContentHeight = self.scrollNode.view.contentSize.height + let previousVerticalOffset = self.scrollNode.view.contentOffset.y + + self.scrollNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.scrollNode.view.contentSize = CGSize(width: 0.0, height: layout.size.height * 3.0) + + if previousContentHeight.isEqual(to: 0.0) { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: self.scrollNode.view.contentSize.height / 3.0) + } else { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: previousVerticalOffset * self.scrollNode.view.contentSize.height / previousContentHeight) + } + var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -199,7 +239,7 @@ final class ItemListNode: ASDisplayNode { insets.top += navigationBarHeight self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height + layout.size.height / 2.0) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -231,9 +271,9 @@ final class ItemListNode: ASDisplayNode { 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 } } } @@ -241,12 +281,12 @@ final class ItemListNode: ASDisplayNode { if let updateStyle = transition.updateStyle { self.listStyle = updateStyle - if let theme = self.theme { + 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 } } } @@ -312,4 +352,28 @@ final class ItemListNode: ASDisplayNode { func scrollToTop() { self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 + + let transition = 1.0 - min(1.0, max(0.0, abs(distanceFromEquilibrium) / 50.0)) + + self.updateNavigationOffset(-distanceFromEquilibrium) + + /*if let toolbarNode = toolbarNode { + toolbarNode.layer.position = CGPoint(x: toolbarNode.layer.position.x, y: self.bounds.size.height - toolbarNode.bounds.size.height / 2.0 + (1.0 - transition) * toolbarNode.bounds.size.height) + }*/ + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + targetContentOffset.pointee = scrollView.contentOffset + + let scrollVelocity = scrollView.panGestureRecognizer.velocity(in: scrollView) + + if abs(scrollVelocity.y) > 200.0 { + self.animateOut() + } else { + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: self.scrollNode.view.contentSize.height / 3.0), animated: true) + } + } } diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index b42a2baa6d..95a1bc6ab0 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -162,7 +162,7 @@ class ItemListDisclosureItemNode: ListViewItemNode { } let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: width - 40 - titleLayout.size.width - 10.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -233,11 +233,11 @@ class ItemListDisclosureItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 12.0), size: labelLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) if let arrowImage = strongSelf.arrowNode.image { - strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 18.0), size: arrowImage.size) + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 15.0), size: arrowImage.size) } switch item.disclosureStyle { @@ -295,6 +295,10 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } diff --git a/TelegramUI/ItemListItem.swift b/TelegramUI/ItemListItem.swift index c2a09126f0..7c69f63423 100644 --- a/TelegramUI/ItemListItem.swift +++ b/TelegramUI/ItemListItem.swift @@ -8,6 +8,7 @@ protocol ItemListItem { var sectionId: ItemListSectionId { get } var tag: ItemListItemTag? { get } var isAlwaysPlain: Bool { get } + var requestsNoInset: Bool { get } } extension ItemListItem { @@ -18,6 +19,10 @@ extension ItemListItem { var tag: ItemListItemTag? { return nil } + + var requestsNoInset: Bool { + return false + } } protocol ItemListItemNode { @@ -30,7 +35,7 @@ protocol ItemListItemFocusableNode { enum ItemListNeighbor { case none - case otherSection + case otherSection(requestsNoInset: Bool) case sameSection(alwaysPlain: Bool) } @@ -43,7 +48,7 @@ func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottomItem: I let topNeighbor: ItemListNeighbor if let topItem = topItem { if topItem.sectionId != item.sectionId { - topNeighbor = .otherSection + topNeighbor = .otherSection(requestsNoInset: topItem.requestsNoInset) } else { topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain) } @@ -54,7 +59,7 @@ func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottomItem: I let bottomNeighbor: ItemListNeighbor if let bottomItem = bottomItem { if bottomItem.sectionId != item.sectionId { - bottomNeighbor = .otherSection + bottomNeighbor = .otherSection(requestsNoInset: bottomItem.requestsNoInset) } else { bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain) } @@ -89,8 +94,12 @@ func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors) -> UIEdgeIns topInset = UIScreenPixel + 35.0 case .sameSection: topInset = 0.0 - case .otherSection: - topInset = UIScreenPixel + 35.0 + case let .otherSection(requestsNoInset): + if requestsNoInset { + topInset = 0.0 + } else { + topInset = UIScreenPixel + 35.0 + } } let bottomInset: CGFloat switch neighbors.bottom { diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index 31079ea66c..ed0426f0e2 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -185,7 +185,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index 1d2b6b336e..b30ccc1908 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -187,7 +187,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 12.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 11.0), size: titleLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index ab02383b00..08c0522d2b 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -448,7 +448,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 32.0), size: statusLayout.size)) - transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 15.0, y: 12.0), size: CGSize(width: 34.0, height: 34.0))) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 15.0, y: 11.0), size: CGSize(width: 34.0, height: 34.0))) if let updatedImageSignal = updatedImageSignal { strongSelf.imageNode.setSignal(account: item.account, signal: updatedImageSignal) diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 2f787028a5..f969a62748 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -230,7 +230,7 @@ class ItemListSwitchItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) if let switchView = strongSelf.switchNode.view as? UISwitch { if strongSelf.switchNode.bounds.size.width.isZero { switchView.sizeToFit() diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index 48b8e0dda3..a3c4f201fa 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -36,7 +36,7 @@ func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: controller.presentOverlayController = { [weak legacyController] controller in if let legacyController = legacyController { let childController = LegacyController(legacyController: controller!, presentation: .custom) - legacyController.present(childController, in: .window) + legacyController.present(childController, in: .window(.root)) return { [weak childController] in childController?.dismiss() } @@ -99,7 +99,7 @@ func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: menuController?.dismiss(animated: false) } - parentController.present(legacyController, in: .window) + parentController.present(legacyController, in: .window(.root)) /* diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 5f15faba49..c315f3391c 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -87,7 +87,7 @@ private final class LegacyControllerApplicationInterface: NSObject, TGLegacyAppl public func makeOverlayControllerWindow(_ parentController: TGViewController!, contentController: TGOverlayController!, keepKeyboard: Bool) -> TGOverlayControllerWindow! { return LegacyOverlayWindowHost(presentInWindow: { [weak self] c in - self?.controller?.present(c, in: .window) + self?.controller?.present(c, in: .window(.root)) }, parentController: parentController, contentController: contentController, keepKeyboard: keepKeyboard) } } diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift index ecfaff3751..f84d5161f9 100644 --- a/TelegramUI/LegacyLocationController.swift +++ b/TelegramUI/LegacyLocationController.swift @@ -37,7 +37,7 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco let shareController = ShareController(account: account, shareAction: { peerIds in shareAction?(peerIds) }, defaultAction: nil) - legacyController.present(shareController, in: .window) + legacyController.present(shareController, in: .window(.root)) shareAction = { [weak shareController] peerIds in shareController?.dismiss() diff --git a/TelegramUI/LocalAuth.swift b/TelegramUI/LocalAuth.swift new file mode 100644 index 0000000000..478c784512 --- /dev/null +++ b/TelegramUI/LocalAuth.swift @@ -0,0 +1,31 @@ +import Foundation +import LocalAuthentication +import SwiftSignalKit + +struct LocalAuth { + static let isTouchIDAvailable: Bool = { + return LAContext().canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) + }() + + static func auth(reason: String) -> Signal { + return Signal { subscriber in + let context = LAContext() + + if LAContext().canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) { + context.evaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, localizedReason: reason, reply: { result, _ in + subscriber.putNext(result) + subscriber.putCompletion() + }) + } else { + subscriber.putNext(false) + subscriber.putCompletion() + } + + return ActionDisposable { + if #available(iOSApplicationExtension 9.0, *) { + context.invalidate() + } + } + } + } +} diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift index b337830732..3a82f96221 100644 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -76,6 +76,7 @@ enum AudioPlaylistPlayback { enum AudioPlaylistControl { case navigation(AudioPlaylistNavigation) case playback(AudioPlaylistPlayback) + case stop } protocol AudioPlaylistId { @@ -119,14 +120,50 @@ struct AudioPlaylistStateAndStatus: Equatable { private enum AudioPlaylistItemPlayer { case player(MediaPlayer) - case videoContext(ManagedMediaId, MediaResource, MediaPlayer, Disposable) + case sharedVideo(InstantVideoNode) - var player: MediaPlayer { + func play() { switch self { case let .player(player): - return player - case let .videoContext(_, _, player, _): - return 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) } } } @@ -150,45 +187,40 @@ private final class AudioPlaylistInternalState { 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<(ManagedMediaId, MediaResource, Disposable)?>(nil) + private let overlayContextValue = Promise(nil) var stateAndStatus: Signal { return self.currentStateAndStatusValue.get() } - var currentContext: (ManagedMediaId, MediaResource, Disposable)? - + var currentVideoNode: InstantVideoNode? var overlayContextDisposable: Disposable? - init(audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, mediaManager: MediaManager, postbox: Postbox, playlist: AudioPlaylist) { + 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] context in + self.overlayContextDisposable = (self.overlayContextValue.get() |> deliverOnMainQueue).start(next: { [weak self] node in if let strongSelf = self { - var updated = false - if let lhsId = strongSelf.currentContext?.0, let rhsId = context?.0 { - updated = !lhsId.isEqual(to: rhsId) - } else if (strongSelf.currentContext?.0 != nil) != (context?.0 != nil) { - updated = true - } - if updated { - strongSelf.currentContext?.2.dispose() - if let id = strongSelf.currentContext?.0 { - strongSelf.overlayMediaManager.controller?.removeVideoContext(id: id) + if strongSelf.currentVideoNode !== node { + if let currentVideoNode = strongSelf.currentVideoNode { + currentVideoNode.setSoundEnabled(false) + strongSelf.overlayMediaManager.controller?.removeNode(currentVideoNode) } - strongSelf.currentContext = context - if let (id, resource, _) = context, let mediaManager = strongSelf.mediaManager { - strongSelf.overlayMediaManager.controller?.addVideoContext(mediaManager: mediaManager, postbox: postbox, id: id, resource: resource, priority: 0) + strongSelf.currentVideoNode = node + if let node = node { + strongSelf.overlayMediaManager.controller?.addNode(node) } } } @@ -197,10 +229,10 @@ final class ManagedAudioPlaylistPlayer { deinit { self.overlayContextDisposable?.dispose() - if let id = self.currentContext?.0 { - self.overlayMediaManager.controller?.removeVideoContext(id: id) + if let currentVideoNode = self.currentVideoNode { + self.overlayMediaManager.controller?.removeNode(currentVideoNode) + currentVideoNode.setSoundEnabled(false) } - self.currentContext?.2.dispose() self.currentState.with { state -> Void in state.navigationDisposable.dispose() } @@ -213,13 +245,13 @@ final class ManagedAudioPlaylistPlayer { if let item = state.currentItem { switch playback { case .play: - item.player?.player.play() + item.player?.play() case .pause: - item.player?.player.pause() + item.player?.pause() case .togglePlayPause: - item.player?.player.togglePlayPause() + item.player?.togglePlayPause() case let .seek(timestamp): - item.player?.player.seek(timestamp: timestamp) + item.player?.seek(timestamp) } } } @@ -232,20 +264,20 @@ final class ManagedAudioPlaylistPlayer { } let postbox = self.postbox let audioSessionManager = self.audioSessionManager - let overlayMediaManager = self.overlayMediaManager 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: (MediaResource, MessageId)? + 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.resource, message.id) + instantVideo = (file, message.id, message.stableId) } } } @@ -267,15 +299,16 @@ final class ManagedAudioPlaylistPlayer { if let resource = item.resource { var itemPlayer: AudioPlaylistItemPlayer? if let instantVideo = instantVideo { - if let mediaManager = mediaManager { - let (player, disposable) = mediaManager.videoContext(postbox: postbox, id: PeerMessageManagedMediaId(messageId: instantVideo.1), resource: instantVideo.0, preferSoftwareDecoding: false, backgroundThread: false, priority: -1, initiatePlayback: true, activate: { _ in - }, deactivate: { - return .complete() - }) - itemPlayer = .videoContext(PeerMessageManagedMediaId(messageId: instantVideo.1), instantVideo.0, player, disposable) + if let mediaManager = mediaManager, let account = account { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let videoNode = InstantVideoNode(theme: presentationData.theme, manager: mediaManager, account: account, source: .messageMedia(stableId: instantVideo.2, file: instantVideo.0), priority: 0, withSound: true) + videoNode.tapped = { [weak videoNode] in + videoNode?.togglePlayPause() + } + itemPlayer = .sharedVideo(videoNode) } } else { - let player = MediaPlayer(audioSessionManager: audioSessionManager, overlayMediaManager: overlayMediaManager, postbox: postbox, resource: resource, streamable: item.streamable, video: false, preferSoftwareDecoding: false, enableSound: true) + let player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: resource, streamable: item.streamable, video: false, preferSoftwareDecoding: false, enableSound: true) itemPlayer = .player(player) } return .single((item, AudioPlaylistItemState(item: item, player: itemPlayer))) @@ -290,6 +323,7 @@ final class ManagedAudioPlaylistPlayer { 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): @@ -299,33 +333,53 @@ final class ManagedAudioPlaylistPlayer { strongSelf.control(.navigation(.next)) } }) - case let .videoContext(_, _, player, _): - player.actionAtEnd = .loopDisablingSound({ - 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)) + } } - }) - player.playOnceWithSound() + } + 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: itemState.player?.player.status) + 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: (ManagedMediaId, MediaResource, Disposable)? + var overlayContextValue: InstantVideoNode? if let (_, itemState) = next { - if let player = itemState.player, case let .videoContext(id, resource, _, disposable) = player { - overlayContextValue = (id, resource, disposable) + 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/ManagedVideoNode.swift b/TelegramUI/ManagedVideoNode.swift index d84bc6d0ca..574aea10b1 100644 --- a/TelegramUI/ManagedVideoNode.swift +++ b/TelegramUI/ManagedVideoNode.swift @@ -5,6 +5,8 @@ import Postbox import TelegramCore class ManagedVideoNode: ASDisplayNode { + private let thumbnailNode: TransformImageNode + private var videoPlayer: MediaPlayer? private var playerNode: MediaPlayerNode? private let videoContextDisposable = MetaDisposable() @@ -26,7 +28,11 @@ class ManagedVideoNode: ASDisplayNode { self.preferSoftwareDecoding = preferSoftwareDecoding self.backgroundThread = backgroundThread + self.thumbnailNode = TransformImageNode() + super.init() + + self.addSubnode(self.thumbnailNode) } deinit { diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 78ad88ea08..a3fb762918 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -19,34 +19,6 @@ private struct WrappedAudioPlaylistItemId: Hashable, Equatable { } } -private final class ManagedAudioPlaylistPlayerStatusesContext { - private var subscribers: [WrappedAudioPlaylistItemId: Bag<(AudioPlaylistState?) -> Void>] = [:] - - func addSubscriber(id: WrappedAudioPlaylistItemId, _ f: @escaping (AudioPlaylistState?) -> Void) -> Int { - let bag: Bag<(AudioPlaylistState?) -> Void> - if let currentBag = self.subscribers[id] { - bag = currentBag - } else { - bag = Bag() - self.subscribers[id] = bag - } - return bag.add(f) - } - - func removeSubscriber(id: WrappedAudioPlaylistItemId, index: Int) { - if let bag = subscribers[id] { - bag.remove(index) - if bag.isEmpty { - self.subscribers.removeValue(forKey: id) - } - } - } - - func subscribersForId(_ id: WrappedAudioPlaylistItemId) -> [(AudioPlaylistState) -> Void]? { - return self.subscribers[id]?.copyItems() - } -} - struct WrappedManagedMediaId: Hashable { let id: ManagedMediaId @@ -181,11 +153,17 @@ private final class ActiveManagedVideoContext { } } +enum SharedMediaPlayerGroup: Int { + case music = 0 + case voiceAndInstantVideo = 1 +} + public final class MediaManager: NSObject { private let queue = Queue.mainQueue() public let audioSession: ManagedAudioSession let overlayMediaManager = OverlayMediaManager() + let sharedVideoContextManager = SharedVideoContextManager() private let playlistPlayer = Atomic(value: nil) private let playlistPlayerStateAndStatusValue = Promise(nil) @@ -193,7 +171,9 @@ public final class MediaManager: NSObject { return self.playlistPlayerStateAndStatusValue.get() } private let playlistPlayerStateValueDisposable = MetaDisposable() - private let playlistPlayerStatusesContext = Atomic(value: ManagedAudioPlaylistPlayerStatusesContext()) + + private let sharedPlayerByGroup: [SharedMediaPlayerGroup: SharedMediaPlayer] = [:] + private var currentOverlayVideoNode: OverlayMediaItemNode? private let globalControlsStatus = Promise(nil) @@ -314,8 +294,6 @@ public final class MediaManager: NSObject { self.globalControlsStatusDisposable.dispose() } - //func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool = false) -> Disposable { - 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()) @@ -325,7 +303,7 @@ public final class MediaManager: NSObject { if let currentActiveContext = self.managedVideoContexts[wrappedId] { activeContext = currentActiveContext } else { - let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, overlayMediaManager: self.overlayMediaManager, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false) + let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false) mediaPlayer.actionAtEnd = .loop let playerNode = MediaPlayerNode(backgroundThread: backgroundThread) mediaPlayer.attachPlayerNode(playerNode) @@ -402,10 +380,6 @@ public final class MediaManager: NSObject { } } - private func updatePlaylistPlayerStateValue() { - - } - func playlistPlayerControl(_ control: AudioPlaylistControl) { var player: ManagedAudioPlaylistPlayer? self.playlistPlayer.with { currentPlayer -> Void in @@ -425,24 +399,6 @@ public final class MediaManager: NSObject { } return nil } - /*return Signal { subscriber in - let id = WrappedAudioPlaylistItemId(playlistId: playlistId, itemId: itemId) - let index = self.playlistPlayerStatusesContext.with { context -> Int in - context.addSubscriber(id: id, { state in - subscriber.putNext(state) - }) - } - - - - return ActionDisposable { [weak self] in - if let strongSelf = self { - strongSelf.playlistPlayerStatusesContext.with { context -> Void in - context.removeSubscriber(id: id, index: index) - } - } - } - }*/ } @objc func playCommandEvent(_ command: AnyObject) { @@ -464,4 +420,16 @@ public final class MediaManager: NSObject { @objc func togglePlayPauseCommandEvent(_ command: AnyObject) { self.playlistPlayerControl(.playback(.togglePlayPause)) } + + func setOverlayVideoNode(_ node: OverlayMediaItemNode?) { + if let currentOverlayVideoNode = self.currentOverlayVideoNode { + self.overlayMediaManager.controller?.removeNode(currentOverlayVideoNode, customTransition: true) + self.currentOverlayVideoNode = nil + } + + if let node = node { + self.currentOverlayVideoNode = node + self.overlayMediaManager.controller?.addNode(node, customTransition: true) + } + } } diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index ede52b5ae6..9b0e001c30 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -57,7 +57,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { 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, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + 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: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -65,7 +65,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _ in }, navigateToMessage: { _ in }, clickThroughMessage: { }, toggleMessageSelection: { _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _ in }, callPeer: { _ in }, longTap: { _ in }) + }, presentController: { _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }) let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .Music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) listNode.preloadPages = true diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 4205ec2e1b..d8e6c53774 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -40,7 +40,6 @@ enum MediaPlayerActionAtEnd { private final class MediaPlayerContext { private let queue: Queue private let audioSessionManager: ManagedAudioSession - private let overlayMediaManager: OverlayMediaManager private let postbox: Postbox private let resource: MediaResource @@ -60,12 +59,11 @@ private final class MediaPlayerContext { fileprivate var actionAtEnd: MediaPlayerActionAtEnd = .stop - init(queue: Queue, audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { assert(queue.isCurrent()) self.queue = queue self.audioSessionManager = audioSessionManager - self.overlayMediaManager = overlayMediaManager self.playerStatus = playerStatus self.postbox = postbox self.resource = resource @@ -79,7 +77,7 @@ private final class MediaPlayerContext { self.videoRenderer.visibilityUpdated = { [weak self] value in assert(queue.isCurrent()) - if let strongSelf = self { + if let strongSelf = self, !strongSelf.enableSound { switch strongSelf.state { case .empty: if value { @@ -261,11 +259,7 @@ private final class MediaPlayerContext { renderer = MediaPlayerAudioRenderer(audioSessionManager: self.audioSessionManager, audioPaused: { [weak self] in queue.async { if let strongSelf = self { - if case .loopDisablingSound = strongSelf.actionAtEnd { - strongSelf.continuePlayingWithoutSound() - } else { - strongSelf.pause() - } + strongSelf.pause() } } }) @@ -339,31 +333,34 @@ private final class MediaPlayerContext { fileprivate func playOnceWithSound() { assert(self.queue.isCurrent()) - self.lastStatusUpdateTimestamp = nil - self.enableSound = true - self.seek(timestamp: 0.0, action: .play) + if !self.enableSound { + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + self.seek(timestamp: 0.0, action: .play) + } } fileprivate func continuePlayingWithoutSound() { - self.lastStatusUpdateTimestamp = nil - self.enableSound = true - - var loadedState: MediaPlayerLoadedState? - switch self.state { - case .empty: - break - case let .playing(currentLoadedState): - loadedState = currentLoadedState - case let .paused(currentLoadedState): - loadedState = currentLoadedState - case let .seeking(previousFrameSource, previousTimestamp, previousDisposable, _): + if self.enableSound { + self.lastStatusUpdateTimestamp = nil + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case let .seeking: + self.enableSound = false + } + + if let loadedState = loadedState { self.enableSound = false - } - - if let loadedState = loadedState { - self.enableSound = false - let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) - self.seek(timestamp: timestamp, action: .play) + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) + } } } @@ -663,9 +660,9 @@ final class MediaPlayer { } } - init(audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { + init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, overlayMediaManager: overlayMediaManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: enableSound) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: enableSound) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index 0d1b2c52d0..22f0bc29ed 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -255,12 +255,14 @@ private final class AudioPlayerRendererContext { case .paused: break } - - completion() } + + completion() } fileprivate func start() { + assert(audioPlayerRendererQueue.isCurrent()) + if self.paused { self.paused = false self.startAudioUnit() @@ -268,6 +270,8 @@ private final class AudioPlayerRendererContext { } fileprivate func stop() { + assert(audioPlayerRendererQueue.isCurrent()) + if !self.paused { self.paused = true self.setPlaying(false) @@ -276,6 +280,8 @@ private final class AudioPlayerRendererContext { } private func startAudioUnit() { + assert(audioPlayerRendererQueue.isCurrent()) + if self.audioUnit == nil { var desc = AudioComponentDescription() desc.componentType = kAudioUnitType_Output @@ -331,22 +337,24 @@ private final class AudioPlayerRendererContext { strongSelf.audioSessionAcquired() } } - }, deactivate: { [weak self] in - return Signal { subscriber in - audioPlayerRendererQueue.async { - if let strongSelf = self { - strongSelf.audioPaused() - strongSelf.stop() - subscriber.putCompletion() - } + }, deactivate: { [weak self] in + return Signal { subscriber in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioPaused() + strongSelf.stop() + subscriber.putCompletion() } - - return EmptyDisposable } + + return EmptyDisposable + } })) } private func audioSessionAcquired() { + assert(audioPlayerRendererQueue.isCurrent()) + if let audioUnit = self.audioUnit { guard AudioOutputUnitStart(audioUnit) == noErr else { self.closeAudioUnit() @@ -480,6 +488,8 @@ private final class AudioPlayerRendererContext { } fileprivate func beginRequestingFrames(queue: DispatchQueue, takeFrame: @escaping () -> MediaTrackFrameResult) { + assert(audioPlayerRendererQueue.isCurrent()) + if let _ = self.requestingFramesContext { return } @@ -490,6 +500,8 @@ private final class AudioPlayerRendererContext { } func endRequestingFrames() { + assert(audioPlayerRendererQueue.isCurrent()) + self.requestingFramesContext = nil } } diff --git a/TelegramUI/MediaPlayerNode.swift b/TelegramUI/MediaPlayerNode.swift index 4030a9fe7f..524058804e 100644 --- a/TelegramUI/MediaPlayerNode.swift +++ b/TelegramUI/MediaPlayerNode.swift @@ -94,9 +94,10 @@ final class MediaPlayerNode: ASDisplayNode { videoLayer.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(rotationAngle))) } - if requestFrames { - //print("request") - self.startPolling() + if self.videoInHierarchy { + if requestFrames { + self.startPolling() + } } } } @@ -113,7 +114,7 @@ final class MediaPlayerNode: ASDisplayNode { switch status { case let .delay(delay): strongSelf.timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { - if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _) = strongSelf.state, requestFrames { + if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _) = strongSelf.state, requestFrames, strongSelf.videoInHierarchy { if videoLayer.isReadyForMoreMediaData { strongSelf.timer?.invalidate() strongSelf.timer = nil @@ -223,7 +224,9 @@ final class MediaPlayerNode: ASDisplayNode { if let strongSelf = self { if strongSelf.videoInHierarchy != value { strongSelf.videoInHierarchy = value - //strongSelf.videoNode.playerLayer.flush() + if value { + strongSelf.updateState() + } } strongSelf.updateVideoInHierarchy?(value) } @@ -246,6 +249,7 @@ final class MediaPlayerNode: ASDisplayNode { deinit { assert(Queue.mainQueue().isCurrent()) + self.videoLayer?.removeFromSuperlayer() if let (takeFrameQueue, _) = self.takeFrameAndQueue { if let videoLayer = self.videoLayer { diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index 8cb56654a2..cf5b45efc6 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -98,6 +98,7 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { private var scrubbingTimestamp: Double? var playbackStatusUpdated: ((MediaPlayerPlaybackStatus?) -> Void)? + var playerStatusUpdated: ((MediaPlayerStatus?) -> Void)? var seek: ((Double) -> Void)? private var statusValue: MediaPlayerStatus? { @@ -112,6 +113,8 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { playbackStatusUpdated(playbackStatus) } } + + self.playerStatusUpdated?(self.statusValue) } } } @@ -307,28 +310,54 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") } } else if let statusValue = self.statusValue, !progress.isNaN && progress.isFinite { - let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) - let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - - let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) - let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - - self.foregroundNode.frame = toRect - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") - self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") - - if let handleNodeContainer = self.handleNodeContainer { - let fromBounds = bounds - let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + if statusValue.generationTimestamp.isZero { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - handleNodeContainer.isHidden = false - handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: statusValue.duration, beginTime: statusValue.generationTimestamp, offset: statusValue.timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") - } - - if let handleNode = self.handleNode { - let fromPosition = handleNode.position - let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) - handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0, repeatForever: true), forKey: "playback-position") + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + self.foregroundNode.frame = toRect + self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + + if let handleNodeContainer = self.handleNodeContainer { + let fromBounds = bounds + let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + + handleNodeContainer.isHidden = false + handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + } + + if let handleNode = self.handleNode { + let fromPosition = handleNode.position + let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) + handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: nil, offset: timestamp, speed: 0.0, repeatForever: true), forKey: "playback-position") + } + } else { + let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + + let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) + let toBounds = CGRect(origin: CGPoint(), size: toRect.size) + + self.foregroundNode.frame = toRect + self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + self.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") + + if let handleNodeContainer = self.handleNodeContainer { + let fromBounds = bounds + let toBounds = bounds.offsetBy(dx: -bounds.size.width, dy: 0.0) + + handleNodeContainer.isHidden = false + handleNodeContainer.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: statusValue.duration, beginTime: statusValue.generationTimestamp, offset: statusValue.timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + } + + if let handleNode = self.handleNode { + let fromPosition = handleNode.position + let toPosition = CGPoint(x: fromPosition.x - 1.0, y: fromPosition.y) + handleNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: fromPosition), to: NSValue(cgPoint: toPosition), duration: duration / Double(bounds.size.width), beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0, repeatForever: true), forKey: "playback-position") + } } } else { self.handleNodeContainer?.isHidden = true diff --git a/TelegramUI/NSDictionary+Stripe.h b/TelegramUI/NSDictionary+Stripe.h new file mode 100755 index 0000000000..89294fd5fd --- /dev/null +++ b/TelegramUI/NSDictionary+Stripe.h @@ -0,0 +1,17 @@ +// +// NSDictionary+Stripe.h +// Stripe +// +// Created by Jack Flintermann on 10/15/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface NSDictionary (Stripe) + +- (nullable NSDictionary *)stp_dictionaryByRemovingNullsValidatingRequiredFields:(nonnull NSArray *)requiredFields; + +@end + +void linkNSDictionaryCategory(void); diff --git a/TelegramUI/NSDictionary+Stripe.m b/TelegramUI/NSDictionary+Stripe.m new file mode 100755 index 0000000000..343d8cdc6f --- /dev/null +++ b/TelegramUI/NSDictionary+Stripe.m @@ -0,0 +1,30 @@ +// +// NSDictionary+Stripe.m +// Stripe +// +// Created by Jack Flintermann on 10/15/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "NSDictionary+Stripe.h" + +@implementation NSDictionary (Stripe) + +- (nullable NSDictionary *)stp_dictionaryByRemovingNullsValidatingRequiredFields:(nonnull NSArray *)requiredFields { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) { + if (obj != [NSNull null]) { + dict[key] = obj; + } + }]; + for (NSString *key in requiredFields) { + if (![[dict allKeys] containsObject:key]) { + return nil; + } + } + return [dict copy]; +} + +@end + +void linkNSDictionaryCategory(void){} diff --git a/TelegramUI/NSString+Stripe.h b/TelegramUI/NSString+Stripe.h new file mode 100755 index 0000000000..493a22b311 --- /dev/null +++ b/TelegramUI/NSString+Stripe.h @@ -0,0 +1,19 @@ +// +// NSString+Stripe.h +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface NSString (Stripe) + +- (NSString *)stp_safeSubstringToIndex:(NSUInteger)index; +- (NSString *)stp_safeSubstringFromIndex:(NSUInteger)index; +- (NSString *)stp_reversedString; + +@end + +void linkNSStringCategory(void); diff --git a/TelegramUI/NSString+Stripe.m b/TelegramUI/NSString+Stripe.m new file mode 100755 index 0000000000..732a7e1181 --- /dev/null +++ b/TelegramUI/NSString+Stripe.m @@ -0,0 +1,33 @@ +// +// NSString+Stripe.m +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "NSString+Stripe.h" + +@implementation NSString (Stripe) + +- (NSString *)stp_safeSubstringToIndex:(NSUInteger)index { + return [self substringToIndex:MIN(self.length, index)]; +} + +- (NSString *)stp_safeSubstringFromIndex:(NSUInteger)index { + return (index > self.length) ? @"" : [self substringFromIndex:index]; +} + +- (NSString *)stp_reversedString { + NSMutableString *mutableReversedString = [NSMutableString stringWithCapacity:self.length]; + [self enumerateSubstringsInRange:NSMakeRange(0, self.length) + options:NSStringEnumerationReverse | NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop) { + [mutableReversedString appendString:substring]; + }]; + return [mutableReversedString copy]; +} + +@end + +void linkNSStringCategory(void){} diff --git a/TelegramUI/NSString+Stripe_CardBrands.h b/TelegramUI/NSString+Stripe_CardBrands.h new file mode 100755 index 0000000000..2fa96ef159 --- /dev/null +++ b/TelegramUI/NSString+Stripe_CardBrands.h @@ -0,0 +1,18 @@ +// +// NSString+Stripe_CardBrands.h +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import "STPCardBrand.h" + +@interface NSString (Stripe_CardBrands) + ++ (nonnull instancetype)stp_stringWithCardBrand:(STPCardBrand)brand; + +@end + +void linkNSStringCardBrandsCategory(void); diff --git a/TelegramUI/NSString+Stripe_CardBrands.m b/TelegramUI/NSString+Stripe_CardBrands.m new file mode 100755 index 0000000000..b5961d2a4c --- /dev/null +++ b/TelegramUI/NSString+Stripe_CardBrands.m @@ -0,0 +1,27 @@ +// +// NSString+Stripe_CardBrands.m +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "NSString+Stripe_CardBrands.h" + +@implementation NSString (Stripe_CardBrands) + ++ (nonnull instancetype)stp_stringWithCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: return @"American Express"; + case STPCardBrandDinersClub: return @"Diners Club"; + case STPCardBrandDiscover: return @"Discover"; + case STPCardBrandJCB: return @"JCB"; + case STPCardBrandMasterCard: return @"MasterCard"; + case STPCardBrandUnknown: return @"Unknown"; + case STPCardBrandVisa: return @"Visa"; + } +} + +@end + +void linkNSStringCardBrandsCategory(void){} diff --git a/TelegramUI/NetworkUsageStatsController.swift b/TelegramUI/NetworkUsageStatsController.swift index 79ca9672ae..db52bc7940 100644 --- a/TelegramUI/NetworkUsageStatsController.swift +++ b/TelegramUI/NetworkUsageStatsController.swift @@ -420,7 +420,7 @@ func networkUsageStatsController(account: Account) -> ViewController { } presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } return controller diff --git a/TelegramUI/Notices.swift b/TelegramUI/Notices.swift new file mode 100644 index 0000000000..0d66096b1c --- /dev/null +++ b/TelegramUI/Notices.swift @@ -0,0 +1,54 @@ +import Foundation +import Postbox +import SwiftSignalKit + +final class ApplicationSpecificBoolNotice: Coding { + init() { + } + + init(decoder: Decoder) { + } + + func encode(_ encoder: Encoder) { + } +} + +private func noticeNamespace(namespace: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: namespace) + return key +} + +private func noticeKey(peerId: PeerId, key: Int32) -> ValueBoxKey { + let v = ValueBoxKey(length: 8 + 4) + v.setInt64(0, value: peerId.toInt64()) + v.setInt32(8, value: key) + return v +} + +private struct ApplicationSpecificNoticeKeys { + private static let botPaymentLiabilityNamespace: Int32 = 1 + + static func botPaymentLiabilityNotice(peerId: PeerId) -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: botPaymentLiabilityNamespace), key: noticeKey(peerId: peerId, key: 0)) + } +} + +struct ApplicationSpecificNotice { + static func getBotPaymentLiability(postbox: Postbox, peerId: PeerId) -> Signal { + return postbox.modify { modifier -> Bool in + if let _ = modifier.getNoticeEntry(key: ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId)) as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } + } + + static func setBotPaymentLiability(postbox: Postbox, peerId: PeerId) -> Signal { + return postbox.modify { modifier -> Void in + modifier.setNoticeEntry(key: ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId), value: ApplicationSpecificBoolNotice()) + } + } +} + diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index 6718796fd8..53e22cabf9 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -175,6 +175,7 @@ public func notificationSoundSelectionController(account: Account, isModal: Bool } let controller = ItemListController(account: account, state: signal) + controller.enableInteractiveDismiss = true let result = Promise() diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 461dabf36c..8396cf0aa6 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -423,7 +423,7 @@ public func notificationsAndSoundsController(account: Account) -> ViewController let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window, with: a) + controller?.present(c, in: .window(.root), with: a) } return controller } diff --git a/TelegramUI/OverlayMediaController.swift b/TelegramUI/OverlayMediaController.swift index 6f1464fd81..8794b5c13a 100644 --- a/TelegramUI/OverlayMediaController.swift +++ b/TelegramUI/OverlayMediaController.swift @@ -24,17 +24,18 @@ public final class OverlayMediaController: ViewController { self.displayNodeDidLoad() } - func addVideoContext(mediaManager: MediaManager, postbox: Postbox, id: ManagedMediaId, resource: MediaResource, priority: Int32) { - self.controllerNode.addVideoContext(mediaManager: mediaManager, postbox: postbox, id: id, resource: resource, priority: priority) + func addNode(_ node: OverlayMediaItemNode, customTransition: Bool = false) { + self.controllerNode.addNode(node, customTransition: customTransition) } - func removeVideoContext(id: ManagedMediaId) { - self.controllerNode.removeVideoContext(id: id) + func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool = false) { + self.controllerNode.removeNode(node, customTransition: customTransition) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, transition: transition) + let updatedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: 20.0 + 44.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight) + self.controllerNode.containerLayoutUpdated(updatedLayout, transition: transition) } } diff --git a/TelegramUI/OverlayMediaControllerNode.swift b/TelegramUI/OverlayMediaControllerNode.swift index eeae3600e3..f591cca721 100644 --- a/TelegramUI/OverlayMediaControllerNode.swift +++ b/TelegramUI/OverlayMediaControllerNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit import SwiftSignalKit import Postbox -private final class NotificationContainerControllerNodeView: UITracingLayerView { +private final class OverlayMediaControllerNodeView: UITracingLayerView { var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -12,167 +12,340 @@ private final class NotificationContainerControllerNodeView: UITracingLayerView } } -private final class OverlayVideoContext { - var player: MediaPlayer? - let disposable = MetaDisposable() - var playerNode: MediaPlayerNode? +private final class OverlayMediaVideoNodeData { + var node: OverlayMediaItemNode + var location: CGPoint + var isMinimized: Bool - deinit { - self.disposable.dispose() + init(node: OverlayMediaItemNode, location: CGPoint, isMinimized: Bool) { + self.node = node + self.location = location + self.isMinimized = isMinimized } } -final class OverlayMediaControllerNode: ASDisplayNode { - private var videoContexts: [WrappedManagedMediaId: OverlayVideoContext] = [:] +final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { + private var videoNodes: [OverlayMediaVideoNodeData] = [] private var validLayout: ContainerViewLayout? + private var locationByGroup: [OverlayMediaItemNodeGroup: CGPoint] = [:] + + private weak var draggingNode: OverlayMediaItemNode? + private var draggingStartPosition = CGPoint() + override init() { super.init(viewBlock: { - return NotificationContainerControllerNodeView() + return OverlayMediaControllerNodeView() }, didLoad: nil) - (self.view as! NotificationContainerControllerNodeView).hitTestImpl = { [weak self] point, event in + (self.view as! OverlayMediaControllerNodeView).hitTestImpl = { [weak self] point, event in return self?.hitTest(point, with: event) } + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.cancelsTouchesInView = false + panRecognizer.delegate = self + self.view.addGestureRecognizer(panRecognizer) } deinit { - for (_, context) in self.videoContexts { - context.disposable.dispose() - } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for item in self.videoNodes { + if item.node.frame.contains(point) { + if let result = item.node.hitTest(point.offsetBy(dx: -item.node.frame.origin.x, dy: -item.node.frame.origin.y), with: event) { + return result + } else { + return item.node.view + } + } + } return nil } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout - for (_, context) in self.videoContexts { - if let playerNode = context.playerNode { - let videoSize = CGSize(width: 100.0, height: 100.0) - transition.updateFrame(node: playerNode, frame: CGRect(origin: CGPoint(x: layout.size.width - 4.0 - videoSize.width, y: 20.0 + 44.0 + 38.0 + 4.0), size: videoSize)) + for item in self.videoNodes { + let nodeSize = item.node.preferredSizeForOverlayDisplay() + transition.updateFrame(node: item.node, frame: CGRect(origin: self.nodePosition(layout: layout, size: nodeSize, location: item.location, hidden: !item.node.hasAttachedContext, isMinimized: item.isMinimized, tempExtendedTopInset: item.node.tempExtendedTopInset), size: nodeSize)) + item.node.updateLayout(nodeSize) + } + } + + private func nodePosition(layout: ContainerViewLayout, size: CGSize, location: CGPoint, hidden: Bool, isMinimized: Bool, tempExtendedTopInset: Bool) -> CGPoint { + var layoutInsets = layout.insets(options: [.input]) + layoutInsets.bottom += 48.0 + if tempExtendedTopInset { + layoutInsets.top += 38.0 + } + let inset: CGFloat = 4.0 + var result = CGPoint() + if location.x.isZero { + if isMinimized { + result.x = inset - size.width + 40.0 + } else if hidden { + result.x = -size.width - inset + } else { + result.x = inset + } + } else { + if isMinimized { + result.x = layout.size.width - inset - 40.0 + } else if hidden { + result.x = layout.size.width + inset + } else { + result.x = layout.size.width - inset - size.width } } - } - - func addVideoContext(mediaManager: MediaManager, postbox: Postbox, id: ManagedMediaId, resource: MediaResource, priority: Int32) { - let wrappedId = WrappedManagedMediaId(id: id) - if self.videoContexts[wrappedId] == nil { - let context = OverlayVideoContext() - self.videoContexts[wrappedId] = context - let (player, disposable) = mediaManager.videoContext(postbox: postbox, id: id, resource: resource, preferSoftwareDecoding: false, backgroundThread: false, priority: priority, initiatePlayback: true, activate: { [weak self] playerNode in - if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { - if context.playerNode !== playerNode { - if context.playerNode?.supernode === self { - context.playerNode?.removeFromSupernode() - } - - context.playerNode = playerNode - - strongSelf.addSubnode(playerNode) - playerNode.transformArguments = TransformImageArguments(corners: ImageCorners(radius: 50.0), imageSize: CGSize(width: 100.0, height: 100.0), boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()) - if let validLayout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(validLayout, transition: .immediate) - playerNode.layer.animatePosition(from: CGPoint(x: 104.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - } - }, deactivate: { [weak self] in - if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId], let playerNode = context.playerNode { - if let snapshot = playerNode.view.snapshotView(afterScreenUpdates: false) { - snapshot.frame = playerNode.view.frame - strongSelf.view.addSubview(snapshot) - let fromPosition = playerNode.layer.position - playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) - snapshot.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak snapshot] _ in - snapshot?.removeFromSuperview() - }) - } - context.playerNode = nil - if playerNode.supernode === self { - playerNode.removeFromSupernode() - } - return .complete() - } else { - return .complete() - } - - /*return Signal { subscriber in - if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { - if let playerNode = context.playerNode { - let fromPosition = playerNode.layer.position - playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) - context.playerNode = nil - playerNode.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - subscriber.putCompletion() - }) - } else { - subscriber.putCompletion() - } - } else { - subscriber.putCompletion() - } - return EmptyDisposable - }*/ - }) - context.player = player - context.disposable.set(disposable) + if location.y.isZero { + result.y = layoutInsets.top + inset + } else { + result.y = layout.size.height - layoutInsets.bottom - inset - size.height } + return result } - /*func addVideoContext(id: ManagedMediaId, contextSignal: Signal) { - let wrappedId = WrappedManagedMediaId(id: id) - if self.videoContexts[wrappedId] == nil { - let context = OverlayVideoContext() - self.videoContexts[wrappedId] = context + private func nodeLocationForPosition(layout: ContainerViewLayout, position: CGPoint, velocity: CGPoint, size: CGSize, tempExtendedTopInset: Bool) -> (CGPoint, Bool) { + var layoutInsets = layout.insets(options: [.input]) + layoutInsets.bottom += 48.0 + if tempExtendedTopInset { + layoutInsets.top += 38.0 + } + var result = CGPoint() + if position.x < layout.size.width / 2.0 { + result.x = 0.0 + } else { + result.x = 1.0 + } + if position.y < layoutInsets.top + (layout.size.height - layoutInsets.bottom - layoutInsets.top) / 2.0 { + result.y = 0.0 + } else { + result.y = 1.0 + } + + let currentPosition = result + + let TGVideoMessagePIPAngleEpsilon: CGFloat = 30.0 + var shouldHide = false + + if (velocity.x * velocity.x + velocity.y * velocity.y) >= 500.0 * 500.0 { + let x = velocity.x + let y = velocity.y - context.disposable.set((contextSignal |> deliverOnMainQueue).start(next: { [weak self] videoContext in - if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { - if context.video?.playerNode !== videoContext.playerNode { - if context.video?.playerNode?.supernode === self { - context.video?.playerNode?.removeFromSupernode() - } - - context.video = videoContext - - if let playerNode = videoContext.playerNode { - strongSelf.addSubnode(playerNode) - playerNode.transformArguments = TransformImageArguments(corners: ImageCorners(radius: 50.0), imageSize: CGSize(width: 100.0, height: 100.0), boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()) - if let validLayout = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(validLayout, transition: .immediate) - playerNode.layer.animatePosition(from: CGPoint(x: 104.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - } else { - context.video = videoContext - } + var angle = atan2(y, x) * 180.0 / CGFloat.pi * -1.0 + if angle < 0.0 { + angle += 360.0 + } + + if currentPosition.x.isZero && currentPosition.y.isZero { + if ((angle > 0 && angle < 90 - TGVideoMessagePIPAngleEpsilon) || angle > 360 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 1.0 + result.y = 0.0 + } else if (angle > 180 + TGVideoMessagePIPAngleEpsilon && angle < 270 + TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 1.0 + } else if (angle > 270 + TGVideoMessagePIPAngleEpsilon && angle < 360 - TGVideoMessagePIPAngleEpsilon) { + result.x = 1.0 + result.y = 1.0 + } else { + shouldHide = true } - })) + } else if !currentPosition.x.isZero && currentPosition.y.isZero { + if (angle > 90 + TGVideoMessagePIPAngleEpsilon && angle < 180 + TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 0.0 + } + else if (angle > 270 - TGVideoMessagePIPAngleEpsilon && angle < 360 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 1.0 + result.y = 1.0 + } + else if (angle > 180 + TGVideoMessagePIPAngleEpsilon && angle < 270 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 1.0 + } + else + { + shouldHide = true + } + } else if currentPosition.x.isZero && !currentPosition.y.isZero { + if (angle > 90 - TGVideoMessagePIPAngleEpsilon && angle < 180 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 0.0 + } + else if (angle < TGVideoMessagePIPAngleEpsilon || angle > 270 + TGVideoMessagePIPAngleEpsilon) + { + result.x = 1.0 + result.y = 1.0 + } + else if (angle > TGVideoMessagePIPAngleEpsilon && angle < 90 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 1.0 + result.y = 0.0 + } + else if (!shouldHide) + { + shouldHide = true + } + } else if !currentPosition.x.isZero && !currentPosition.y.isZero { + if (angle > TGVideoMessagePIPAngleEpsilon && angle < 90 + TGVideoMessagePIPAngleEpsilon) + { + result.x = 1.0 + result.y = 0.0 + } + else if (angle > 180 - TGVideoMessagePIPAngleEpsilon && angle < 270 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 1.0 + } + else if (angle > 90 + TGVideoMessagePIPAngleEpsilon && angle < 180 - TGVideoMessagePIPAngleEpsilon) + { + result.x = 0.0 + result.y = 0.0 + } + else if (!shouldHide) + { + shouldHide = true + } + } } - }*/ + + return (result, shouldHide) + } - func removeVideoContext(id: ManagedMediaId) { - let wrappedId = WrappedManagedMediaId(id: id) - if let context = self.videoContexts[wrappedId] { - if let playerNode = context.playerNode { - if let snapshot = playerNode.view.snapshotView(afterScreenUpdates: false) { - snapshot.frame = playerNode.view.frame - self.view.addSubview(snapshot) - let fromPosition = playerNode.layer.position - playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) - snapshot.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak snapshot] _ in - snapshot?.removeFromSuperview() + func addNode(_ node: OverlayMediaItemNode, customTransition: Bool) { + var location = CGPoint(x: 1.0, y: 0.0) + if let group = node.group { + if let groupLocation = self.locationByGroup[group] { + location = groupLocation + } + } + self.videoNodes.append(OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false)) + self.addSubnode(node) + if let validLayout = self.validLayout { + let nodeSize = node.preferredSizeForOverlayDisplay() + if self.draggingNode !== node { + if customTransition { + node.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: false, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize) + } else { + node.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: true, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize) + } + } + node.updateLayout(nodeSize) + + self.containerLayoutUpdated(validLayout, transition: .immediate) + } + node.hasAttachedContextUpdated = { [weak self] _ in + if let strongSelf = self, let validLayout = strongSelf.validLayout, !customTransition { + strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring)) + } + } + node.unminimize = { [weak self, weak node] in + if let strongSelf = self, let node = node { + if let index = strongSelf.videoNodes.index(where: { $0.node === node }), let validLayout = strongSelf.validLayout, node !== strongSelf.draggingNode, strongSelf.videoNodes[index].isMinimized { + strongSelf.videoNodes[index].isMinimized = false + node.updateMinimizedEdge(nil, adjusting: true) + strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + node.setShouldAcquireContext(true) + } + + func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) { + if node.supernode === self { + node.hasAttachedContextUpdated = nil + node.setShouldAcquireContext(false) + if let index = self.videoNodes.index(where: { $0.node === node }), let validLayout = self.validLayout { + if customTransition { + node.removeFromSupernode() + } else { + let nodeSize = node.preferredSizeForOverlayDisplay() + node.layer.animateFrame(from: node.layer.frame, to: CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: true, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } - - context.playerNode = nil - playerNode.removeFromSupernode() - + } else { + node.removeFromSupernode() } - context.disposable.dispose() - self.videoContexts.removeValue(forKey: wrappedId) + if let index = self.videoNodes.index(where: { $0.node === node }) { + self.videoNodes.remove(at: index) + } + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.index(where: { $0.node === draggingNode }){ + let nodeSize = draggingNode.preferredSizeForOverlayDisplay() + let previousFrame = draggingNode.frame + draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) + draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.draggingNode = nil + } + loop: for item in self.videoNodes { + if item.node.frame.contains(recognizer.location(in: self.view)) { + self.draggingNode = item.node + self.draggingStartPosition = item.node.frame.origin + break loop + } + } + case .changed: + if let draggingNode = self.draggingNode, let validLayout = self.validLayout { + let translation = recognizer.translation(in: self.view) + var nodeFrame = draggingNode.frame + nodeFrame.origin = self.draggingStartPosition.offsetBy(dx: translation.x, dy: translation.y) + if nodeFrame.midX < 0.0 { + draggingNode.updateMinimizedEdge(.left, adjusting: true) + } else if nodeFrame.midX > validLayout.size.width { + draggingNode.updateMinimizedEdge(.right, adjusting: true) + } else { + draggingNode.updateMinimizedEdge(nil, adjusting: true) + } + draggingNode.frame = nodeFrame + } + case .ended, .cancelled: + if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.index(where: { $0.node === draggingNode }){ + let nodeSize = draggingNode.preferredSizeForOverlayDisplay() + let previousFrame = draggingNode.frame + + let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: previousFrame.origin, velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) + + if shouldDismiss && draggingNode.isMinimizeable { + draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) + self.videoNodes[index].isMinimized = true + } else { + draggingNode.updateMinimizedEdge(nil, adjusting: true) + self.videoNodes[index].isMinimized = false + } + + if let group = draggingNode.group { + self.locationByGroup[group] = updatedLocation + } + self.videoNodes[index].location = updatedLocation + + draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize) + draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.draggingNode = nil + + if shouldDismiss && !draggingNode.isMinimizeable { + draggingNode.dismiss() + } + } + default: + break } } } diff --git a/TelegramUI/OverlayMediaItem.swift b/TelegramUI/OverlayMediaItem.swift deleted file mode 100644 index f7e554b6cb..0000000000 --- a/TelegramUI/OverlayMediaItem.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import AsyncDisplayKit - -protocol OverlayMediaItem: class { - func node() -> OverlayMediaItemNode -} - -class OverlayMediaItemNode: ASDisplayNode { - -} diff --git a/TelegramUI/OverlayMediaItemNode.swift b/TelegramUI/OverlayMediaItemNode.swift new file mode 100644 index 0000000000..151c09853e --- /dev/null +++ b/TelegramUI/OverlayMediaItemNode.swift @@ -0,0 +1,62 @@ +import Foundation +import AsyncDisplayKit + +struct OverlayMediaItemNodeGroup: Hashable, RawRepresentable { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + var hashValue: Int { + return self.rawValue.hashValue + } +} + +enum OverlayMediaItemMinimizationEdge { + case left + case right +} + +class OverlayMediaItemNode: ASDisplayNode { + var hasAttachedContextUpdated: ((Bool) -> Void)? + var hasAttachedContext: Bool = false + + var unminimize: (() -> Void)? + + var group: OverlayMediaItemNodeGroup? { + return nil + } + + var tempExtendedTopInset: Bool { + return false + } + + var isMinimizeable: Bool { + return false + } + + func setShouldAcquireContext(_ value: Bool) { + } + + func preferredSizeForOverlayDisplay() -> CGSize { + return CGSize(width: 50.0, height: 50.0) + } + + func updateLayout(_ size: CGSize) { + } + + func dismiss() { + } + + func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { + } + + func performCustomTransitionIn() -> Bool { + return false + } + + func performCustomTransitionOut() -> Bool { + return false + } +} diff --git a/TelegramUI/OverlayMediaManager.swift b/TelegramUI/OverlayMediaManager.swift index 2e3dbb7467..3c154e7ad0 100644 --- a/TelegramUI/OverlayMediaManager.swift +++ b/TelegramUI/OverlayMediaManager.swift @@ -3,33 +3,7 @@ import Foundation final class OverlayMediaManager { var controller: OverlayMediaController? - private var items: [(OverlayMediaItem, OverlayMediaItemNode)] = [] - - init() { - - } - func attachOverlayMediaController(_ controller: OverlayMediaController) { self.controller = controller } - - func addItem(_ item: OverlayMediaItem) { - let node = item.node() - self.items.append((item, node)) - - if let controller = self.controller { - node.frame = CGRect(origin: CGPoint(x: 10.0, y: 80.0), size: CGSize(width: 100.0, height: 60.0)) - controller.displayNode.addSubnode(node) - } - } - - func removeItem(_ item: OverlayMediaItem) { - for i in 0 ..< self.items.count { - if item === self.items[i].0 { - self.items[i].1.removeFromSupernode() - self.items.remove(at: i) - break - } - } - } } diff --git a/TelegramUI/PKPayment+Stripe.h b/TelegramUI/PKPayment+Stripe.h new file mode 100755 index 0000000000..3aa35a77be --- /dev/null +++ b/TelegramUI/PKPayment+Stripe.h @@ -0,0 +1,20 @@ +// +// PKPayment+Stripe.h +// Stripe +// +// Created by Ben Guo on 7/2/15. +// + +#import + +@interface PKPayment (Stripe) + +/// Returns true if the instance is a payment from the simulator. +- (BOOL)stp_isSimulated; + +/// Returns a fake transaction identifier with the expected ~-separated format. ++ (NSString *)stp_testTransactionIdentifier; + +@end + +void linkPKPaymentCategory(void); diff --git a/TelegramUI/PKPayment+Stripe.m b/TelegramUI/PKPayment+Stripe.m new file mode 100755 index 0000000000..36120128a4 --- /dev/null +++ b/TelegramUI/PKPayment+Stripe.m @@ -0,0 +1,35 @@ +// +// PKPayment+Stripe.m +// Stripe +// +// Created by Ben Guo on 7/2/15. +// + +#import "PKPayment+Stripe.h" + +@implementation PKPayment (Stripe) + +- (BOOL)stp_isSimulated { + return [self.token.transactionIdentifier isEqualToString:@"Simulated Identifier"]; +} + ++ (NSString *)stp_testTransactionIdentifier { + NSString *uuid = [[NSUUID UUID] UUIDString]; + uuid = [uuid stringByReplacingOccurrencesOfString:@"~" withString:@"" + options:0 + range:NSMakeRange(0, uuid.length)]; + + // Simulated cards don't have enough info yet. For now, use a fake Visa number + NSString *number = @"4242424242424242"; + + // Without the original PKPaymentRequest, we'll need to use fake data here. + NSDecimalNumber *amount = [NSDecimalNumber decimalNumberWithString:@"0"]; + NSString *cents = [@([[amount decimalNumberByMultiplyingByPowerOf10:2] integerValue]) stringValue]; + NSString *currency = @"USD"; + NSString *identifier = [@[@"ApplePayStubs", number, cents, currency, uuid] componentsJoinedByString:@"~"]; + return identifier; +} + +@end + +void linkPKPaymentCategory(void){} diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index 701761f8f6..45b276df53 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -344,7 +344,7 @@ func passcodeOptionsController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 48ca77a5a8..77086b3ce4 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -91,7 +91,7 @@ public class PeerMediaCollectionController: ViewController { if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) } - }) + }, baseNavigationController: nil) strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in if let strongSelf = strongSelf { @@ -112,7 +112,7 @@ public class PeerMediaCollectionController: ViewController { } })) - strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in + strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { var transitionNode: ASDisplayNode? strongSelf.mediaCollectionDisplayNode.historyNode.forEachItemNode { itemNode in @@ -150,7 +150,7 @@ public class PeerMediaCollectionController: ViewController { if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(id) { /*if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) { - strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in if let node = node { return (node, frame) } else { @@ -205,6 +205,7 @@ public class PeerMediaCollectionController: ViewController { }, presentController: { _ in }, callPeer: { _ in }, longTap: { _ in + }, openCheckoutOrReceipt: { _ in }) self.controllerInteraction = controllerInteraction @@ -258,7 +259,7 @@ public class PeerMediaCollectionController: ViewController { actionSheet?.dismissAnimated() }) ])]) - strongSelf.present(actionSheet, in: .window) + strongSelf.present(actionSheet, in: .window(.root)) } })) } @@ -296,7 +297,7 @@ public class PeerMediaCollectionController: ViewController { }) } } - strongSelf.present(controller, in: .window) + strongSelf.present(controller, in: .window(.root)) } } }, updateTextInputState: { _ in diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift new file mode 100644 index 0000000000..185ef751dc --- /dev/null +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -0,0 +1,235 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +private enum PeerMessagesMediaPlaylistLoadAnchor { + case messageId(MessageId) + case index(MessageIndex) +} + +struct MessageMediaPlaylistItemId: Hashable { + let stableId: UInt32 + + var hashValue: Int { + return self.stableId.hashValue + } + + static func ==(lhs: MessageMediaPlaylistItemId, rhs: MessageMediaPlaylistItemId) -> Bool { + return lhs.stableId == rhs.stableId + } +} + +final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { + let message: Message + + init(message: Message) { + self.message = message + } + + var stableId: AnyHashable { + return MessageMediaPlaylistItemId(stableId: message.stableId) + } + + 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 + } + } + } + } + return nil + } + + 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 { + return SharedMediaPlaybackDisplayData.music(title: title, performer: performer) + } + 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 nil + } +} + +private func navigatedMessageFromView(_ view: MessageHistoryView, anchorIndex: MessageIndex, next: Bool) -> Message? { + var index = 0 + for entry in view.entries { + if entry.index.id == anchorIndex.id { + if next { + if index + 1 < view.entries.count { + switch view.entries[index + 1] { + case let .MessageEntry(message, _, _, _): + return message + default: + return nil + } + } else { + return nil + } + } else { + if index != 0 { + switch view.entries[index - 1] { + case let .MessageEntry(message, _, _, _): + return message + default: + return nil + } + } else { + switch view.entries[0] { + case let .MessageEntry(message, _, _, _): + return message + default: + return nil + } + } + } + } + index += 1 + } + if !view.entries.isEmpty { + switch view.entries[0] { + case let .MessageEntry(message, _, _, _): + return message + default: + return nil + } + } else { + return nil + } +} + +enum PeerMessagesMediaPlaylistLocation { + case messages(peerId: PeerId, tagMask: MessageTags, at: MessageId) + case singleMessage(MessageId) +} + +final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { + private let postbox: Postbox + private let network: Network + private let location: PeerMessagesMediaPlaylistLocation + + private let navigationDisposable = MetaDisposable() + + private var currentItem: Message? + private var loadingItem: Bool = false + + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + + init(postbox: Postbox, network: Network, location: PeerMessagesMediaPlaylistLocation) { + assert(Queue.mainQueue().isCurrent()) + + self.postbox = postbox + self.network = network + self.location = location + + switch self.location { + case let .messages(_, _, messageId): + self.loadItem(anchor: .messageId(messageId), lookForward: true) + case let .singleMessage(messageId): + self.loadItem(anchor: .messageId(messageId), lookForward: true) + } + } + + deinit { + self.navigationDisposable.dispose() + } + + func control(_ action: SharedMediaPlaylistControlAction) { + assert(Queue.mainQueue().isCurrent()) + + switch action { + case .next, .previous: + if !self.loadingItem { + if let currentItem = self.currentItem { + let lookForward: Bool + if case .next = action { + lookForward = true + } else { + lookForward = false + } + self.loadItem(anchor: .index(MessageIndex(currentItem)), lookForward: lookForward) + } + } + } + } + + private func updateState() { + self.stateValue.set(.single(SharedMediaPlaylistState(loading: self.loadingItem, item: self.currentItem.flatMap(MessageMediaPlaylistItem.init)))) + } + + private func loadItem(anchor: PeerMessagesMediaPlaylistLoadAnchor, lookForward: Bool) { + self.loadingItem = true + self.updateState() + switch anchor { + case let .messageId(messageId): + self.navigationDisposable.set((self.postbox.messageAtId(messageId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] message in + if let strongSelf = self { + assert(strongSelf.loadingItem) + + strongSelf.loadingItem = false + strongSelf.currentItem = message + strongSelf.updateState() + } + })) + case let .index(index): + switch self.location { + case let .messages(peerId, tagMask, _): + self.navigationDisposable.set((self.postbox.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 10, anchorIndex: index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] view in + if let strongSelf = self { + assert(strongSelf.loadingItem) + + strongSelf.loadingItem = false + strongSelf.currentItem = navigatedMessageFromView(view.0, anchorIndex: index, next: lookForward) + strongSelf.updateState() + } + })) + case .singleMessage: + self.navigationDisposable.set((self.postbox.messageAtId(index.id) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] message in + if let strongSelf = self { + assert(strongSelf.loadingItem) + + strongSelf.loadingItem = false + strongSelf.currentItem = message + strongSelf.updateState() + } + })) + } + } + } +} diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 314f8a49f6..20fbda3099 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -430,6 +430,21 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu } } +func rawMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal { + return chatMessagePhotoDatas(account: account, photo: photo, autoFetchFullSize: true) + |> map { (thumbnailData, fullSizeData, fullSizeComplete) -> UIImage? in + if let fullSizeData = fullSizeData { + if fullSizeComplete { + return UIImage(data: fullSizeData)?.precomposed() + } + } + if let thumbnailData = thumbnailData { + return UIImage(data: thumbnailData)?.precomposed() + } + return nil + } +} + func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessagePhotoDatas(account: account, photo: photo) diff --git a/TelegramUI/PictureInPictureVideoControlsNode.swift b/TelegramUI/PictureInPictureVideoControlsNode.swift new file mode 100644 index 0000000000..3ca80d774e --- /dev/null +++ b/TelegramUI/PictureInPictureVideoControlsNode.swift @@ -0,0 +1,129 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +import TelegramLegacyComponents + +private let leaveImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureLeave")?.precomposed() +private let pauseImage = UIImage(bundleImageName: "Media Gallery/PictureInPicturePause")?.precomposed() +private let playImage = UIImage(bundleImageName: "Media Gallery/PictureInPicturePlay")?.precomposed() +private let closeImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureClose")?.precomposed() + +final class PictureInPictureVideoControlsNode: ASDisplayNode { + private let leave: () -> Void + private let playPause: () -> Void + private let close: () -> Void + + private let leaveButton: TGEmbedPIPButton + private let pauseButton: TGEmbedPIPButton + private let playButton: TGEmbedPIPButton + private let closeButton: TGEmbedPIPButton + + private var playbackStatusValue: MediaPlayerPlaybackStatus? + private var statusValue: MediaPlayerStatus? { + didSet { + if self.statusValue != oldValue { + let playbackStatus = self.statusValue?.status + if self.playbackStatusValue != playbackStatus { + self.playbackStatusValue = playbackStatus + if let playbackStatus = playbackStatus { + switch playbackStatus { + case .paused: + self.playButton.isHidden = false + self.pauseButton.isHidden = true + case .playing: + self.playButton.isHidden = true + self.pauseButton.isHidden = false + case let .buffering(whilePlaying): + if whilePlaying { + self.playButton.isHidden = true + self.pauseButton.isHidden = false + } else { + self.playButton.isHidden = false + self.pauseButton.isHidden = true + } + } + } + } + } + } + } + + private var statusDisposable: Disposable? + private var statusValuePromise = Promise() + + var status: Signal? { + didSet { + if let status = self.status { + self.statusValuePromise.set(status) + } else { + self.statusValuePromise.set(.never()) + } + } + } + + init(leave: @escaping () -> Void, playPause: @escaping () -> Void, close: @escaping () -> Void) { + self.leave = leave + self.playPause = playPause + self.close = close + + self.leaveButton = TGEmbedPIPButton(frame: CGRect(origin: CGPoint(), size: TGEmbedPIPButtonSize)) + self.pauseButton = TGEmbedPIPButton(frame: CGRect(origin: CGPoint(), size: TGEmbedPIPButtonSize)) + self.playButton = TGEmbedPIPButton(frame: CGRect(origin: CGPoint(), size: TGEmbedPIPButtonSize)) + self.closeButton = TGEmbedPIPButton(frame: CGRect(origin: CGPoint(), size: TGEmbedPIPButtonSize)) + + super.init() + + self.leaveButton.setIconImage(leaveImage) + self.pauseButton.setIconImage(pauseImage) + self.playButton.setIconImage(playImage) + self.closeButton.setIconImage(closeImage) + + self.view.addSubview(self.leaveButton) + self.view.addSubview(self.pauseButton) + self.view.addSubview(self.playButton) + self.view.addSubview(self.closeButton) + + self.leaveButton.addTarget(self, action: #selector(self.leavePressed), for: .touchUpInside) + self.playButton.addTarget(self, action: #selector(self.playPausePressed), for: .touchUpInside) + self.pauseButton.addTarget(self, action: #selector(self.playPausePressed), for: .touchUpInside) + self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) + + self.statusDisposable = (self.statusValuePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.statusValue = status + } + }) + } + + deinit { + self.statusDisposable?.dispose() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let forth = floor(size.width / 4.0) + + let buttonSize = TGEmbedPIPButtonSize + + self.leaveButton.frame = CGRect(origin: CGPoint(x: forth - floor(buttonSize.width / 2.0) - 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize) + + self.pauseButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize) + self.playButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize) + + self.closeButton.frame = CGRect(origin: CGPoint(x: self.playButton.frame.origin.x + forth + 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize) + } + + @objc func leavePressed() { + self.leave() + } + + @objc func playPausePressed() { + self.playPause() + } + + @objc func closePressed() { + self.close() + } +} diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index 537fe81161..b6c2612ac9 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -1,5 +1,6 @@ import Foundation import TelegramCore +import Postbox private enum ApplicationSpecificPreferencesKeyValues: Int32 { case inAppNotificationSettings = 0 diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index b757b592d2..d42fb52708 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -2,7 +2,12 @@ import Foundation import Display private func generateArrowImage(_ theme: PresentationTheme) -> UIImage? { - return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/DisclosureArrow"), color: theme.list.disclosureArrowColor) + return generateImage(CGSize(width: 8.0, height: 14.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.disclosureArrowColor.cgColor) + + let _ = try? drawSvgPath(context, path: "M5.41663691,6.58336309 L0,12 L1.16672619,13.1667262 L7.75008928,6.58336309 L1.16672619,0 L0,1.16672619 Z ") + }) } private func generateCheckIcon(_ theme: PresentationTheme) -> UIImage? { diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index 8e99851f84..08e642bf21 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -109,12 +109,12 @@ public final class PresentationStrings { } public let Activity_UploadingPhoto: String public let PrivacySettings_PrivacyTitle: String + public let Settings_LogoutError: String private let _DialogList_PinLimitError: String private let _DialogList_PinLimitError_r: [(Int, NSRange)] public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_PinLimitError, self._DialogList_PinLimitError_r, [_0]) } - public let Settings_LogoutError: String public let Cache_ClearCache: String public let Common_Close: String public let ChangePhoneNumberCode_Called: String @@ -150,6 +150,7 @@ public final class PresentationStrings { public let Group_MessagePhotoUpdated: String public let Message_PinnedInvoice: String public let Login_InfoAvatarAdd: String + public let Conversation_RestrictedMedia: String public let WebSearch_RecentSectionTitle: String private let _CHAT_MESSAGE_TEXT: String private let _CHAT_MESSAGE_TEXT_r: [(Int, NSRange)] @@ -158,8 +159,8 @@ public final class PresentationStrings { } public let Message_Sticker: String public let Channel_Management_Remove: String - public let Paint_Regular: String public let Channel_Username_Help: String + public let Paint_Regular: String private let _Profile_CreateEncryptedChatOutdatedError: String private let _Profile_CreateEncryptedChatOutdatedError_r: [(Int, NSRange)] public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { @@ -167,11 +168,6 @@ public final class PresentationStrings { } public let Login_InactiveHelp: String public let ChatSettings_Security: String - private let _Time_PreciseDate_9: String - private let _Time_PreciseDate_9_r: [(Int, NSRange)] - public func Time_PreciseDate_9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_9, self._Time_PreciseDate_9_r, [_1, _2, _3]) - } private let _PINNED_STICKER: String private let _PINNED_STICKER_r: [(Int, NSRange)] public func PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -204,6 +200,7 @@ public final class PresentationStrings { public let PrivacyLastSeenSettings_NeverShareWith_Placeholder: String public let TwoStepAuth_SetupEmail: String public let Login_ResetAccountProtected_Reset: String + public let SocksProxySetup_Hostname: String private let _MESSAGE_CONTACT: String private let _MESSAGE_CONTACT_r: [(Int, NSRange)] public func MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -246,12 +243,22 @@ public final class PresentationStrings { public let SharedMedia_EmptyLinksText: String public let Channel_Members_Kick: String public let GoogleDrive_FolderIsEmpty: String + private let _Conversation_RestrictedTextTimed: String + private let _Conversation_RestrictedTextTimed_r: [(Int, NSRange)] + public func Conversation_RestrictedTextTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_RestrictedTextTimed, self._Conversation_RestrictedTextTimed_r, [_0]) + } public let Calls_NoCallsPlaceholder: String public let Message_PinnedDeletedMessage: String public let Conversation_PinMessageAlert_OnlyPin: String public let ReportPeer_ReasonOther_Send: String public let Conversation_InstantPagePreview: String public let PasscodeSettings_SimplePasscodeHelp: String + private let _Time_PreciseDate_m9: String + private let _Time_PreciseDate_m9_r: [(Int, NSRange)] + public func Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m9, self._Time_PreciseDate_m9_r, [_1, _2, _3]) + } public let Group_ErrorAddTooMuch: String public let GroupInfo_Title: String public let State_Updating: String @@ -264,8 +271,8 @@ public final class PresentationStrings { } public let UserInfo_PhoneCall: String public let MusicPlayer_VoiceNote: String - public let Paint_Duplicate: String public let Channel_Username_InvalidTaken: String + public let Paint_Duplicate: String private let _Profile_ShareContactGroupFormat: String private let _Profile_ShareContactGroupFormat_r: [(Int, NSRange)] public func Profile_ShareContactGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -276,25 +283,25 @@ public final class PresentationStrings { public let Checkout_LiabilityAlertTitle: String public let GroupInfo_GroupNamePlaceholder: String public let Conversation_InfoBroadcastList: String + private let _Time_PreciseDate_m11: String + private let _Time_PreciseDate_m11_r: [(Int, NSRange)] + public func Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m11, self._Time_PreciseDate_m11_r, [_1, _2, _3]) + } private let _Notification_JoinedGroupByLink: String private let _Notification_JoinedGroupByLink_r: [(Int, NSRange)] public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_JoinedGroupByLink, self._Notification_JoinedGroupByLink_r, [_0]) } - private let _Time_PreciseDate_8: String - private let _Time_PreciseDate_8_r: [(Int, NSRange)] - public func Time_PreciseDate_8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_8, self._Time_PreciseDate_8_r, [_1, _2, _3]) - } - public let Login_HaveNotReceivedCodeInternal: String public let LoginPassword_Title: String + public let Login_HaveNotReceivedCodeInternal: String public let Conversation_PlayVideo: String public let PasscodeSettings_SimplePasscode: String public let Conversation_MicrophoneAccessDisabled: String public let NewContact_Title: String public let Username_CheckingUsername: String - public let Login_ResetAccountProtected_TimerTitle: String public let UserInfo_InviteBotToGroup: String + public let Login_ResetAccountProtected_TimerTitle: String public let Checkout_Email: String public let CheckoutInfo_SaveInfo: String private let _ChangePhoneNumberCode_CallTimer: String @@ -325,6 +332,7 @@ public final class PresentationStrings { public let Conversation_SecretLinkPreviewAlert: String public let Channel_AdminLog_BanEmbedLinks: String public let Cache_Videos: String + public let Call_ReportSkip: String public let NetworkUsageSettings_MediaImageDataSection: String public let TwoStepAuth_GenericHelp: String private let _DialogList_SingleRecordingAudioSuffix: String @@ -352,12 +360,15 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Profile_ShareBotPersonFormat, self._Profile_ShareBotPersonFormat_r, [_0]) } public let SearchImages_SearchImages: String + public let SocksProxySetup_Title: String public let SharedMedia_EmptyMusicText: String public let Cache_ByPeerHeader: String public let Bot_GroupStatusReadsHistory: String public let TwoStepAuth_ResetAccountConfirmation: String public let CallSettings_Always: String public let SearchImages_DownloadCancelled: String + public let Channel_BanUser_Unban: String + public let Message_ImageExpired: String public let Settings_LogoutConfirmationTitle: String public let UserInfo_FirstNamePlaceholder: String public let ChatSettings_AutoPlayAudio: String @@ -412,25 +423,26 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Notification_PinnedRoundMessage, self._Notification_PinnedRoundMessage_r, [_0]) } public let Conversation_DeleteGroup: String - private let _Time_PreciseDate_7: String - private let _Time_PreciseDate_7_r: [(Int, NSRange)] - public func Time_PreciseDate_7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_7, self._Time_PreciseDate_7_r, [_1, _2, _3]) - } + public let Settings_SaveEditedPhotos: String public let Channel_Management_LabelCreator: String private let _Notification_PinnedStickerMessage: String private let _Notification_PinnedStickerMessage_r: [(Int, NSRange)] public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_PinnedStickerMessage, self._Notification_PinnedStickerMessage_r, [_0]) } - public let Settings_SaveEditedPhotos: String public let PhotoEditor_QualityTool: String public let Login_NetworkError: String public let TwoStepAuth_EnterPasswordForgot: String public let Compose_ChannelMembers: String + private let _Channel_AdminLog_CaptionEdited: String + private let _Channel_AdminLog_CaptionEdited_r: [(Int, NSRange)] + public func Channel_AdminLog_CaptionEdited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_CaptionEdited, self._Channel_AdminLog_CaptionEdited_r, [_0]) + } public let Common_Yes: String public let KeyCommand_JumpToPreviousUnreadChat: String public let CheckoutInfo_ReceiverInfoPhone: String + public let SocksProxySetup_TypeNone: String public let GroupInfo_AddParticipantTitle: String private let _CHANNEL_MESSAGE_TEXT: String private let _CHANNEL_MESSAGE_TEXT_r: [(Int, NSRange)] @@ -507,11 +519,21 @@ public final class PresentationStrings { public let Channel_AdminLog_InfoPanelAlertText: String public let Watch_State_WaitingForNetwork: String public let Cache_Photos: String + private let _Channel_AdminLog_MessageUnpinned: String + private let _Channel_AdminLog_MessageUnpinned_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageUnpinned(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageUnpinned, self._Channel_AdminLog_MessageUnpinned_r, [_0]) + } public let Message_PinnedStickerMessage: String public let PhotoEditor_QualityMedium: String public let Privacy_PaymentsClearInfo: String public let PhotoEditor_CurvesRed: String public let Privacy_PaymentsTitle: String + private let _Time_PreciseDate_m8: String + private let _Time_PreciseDate_m8_r: [(Int, NSRange)] + public func Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m8, self._Time_PreciseDate_m8_r, [_1, _2, _3]) + } public let Login_PhoneNumberHelp: String public let User_DeletedAccount: String public let Call_StatusFailed: String @@ -536,27 +558,25 @@ public final class PresentationStrings { public let Compose_NewGroup: String public let TwoStepAuth_EmailPlaceholder: String public let PhotoEditor_ExposureTool: String + public let Conversation_ViewChannel: String public let ChatAdmins_AdminLabel: String public let Contacts_FailedToSendInvitesMessage: String public let Login_Code: String public let Channel_Username_InvalidCharacters: String - public let Calls_CallTabTitle: String public let FeatureDisabled_Oops: String public let Login_InviteButton: String public let ShareMenu_Send: String public let Conversation_InfoGroup: String public let WatchRemote_AlertTitle: String public let Preview_ProfilePhotoTitle: String + public let Calls_CallTabTitle: String + public let Channel_Members_AddBannedErrorAdmin: String public let Checkout_Phone: String public let Channel_SignMessages_Help: String public let Calls_SubmitRating: String public let Camera_FlashOn: String public let Watch_MessageView_Forward: String - private let _Time_PreciseDate_6: String - private let _Time_PreciseDate_6_r: [(Int, NSRange)] - public func Time_PreciseDate_6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_6, self._Time_PreciseDate_6_r, [_1, _2, _3]) - } + public let GroupInfo_ActionPromote: String public let DialogList_You: String public let Weekday_Monday: String public let Watch_Suggestion_Yes: String @@ -610,6 +630,11 @@ public final class PresentationStrings { public let Your_cards_security_code_is_invalid: String public let Tour_StartButton: String public let CheckoutInfo_Title: String + private let _Channel_AdminLog_MessageRestrictedNameUsername: String + private let _Channel_AdminLog_MessageRestrictedNameUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRestrictedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedNameUsername, self._Channel_AdminLog_MessageRestrictedNameUsername_r, [_1, _2]) + } public let ChangePhoneNumberCode_Help: String public let Web_Error: String public let ShareFileTip_Title: String @@ -647,6 +672,11 @@ public final class PresentationStrings { public let TwoStepAuth_SetPasswordHelp: String public let Channel_AdminLogFilter_EventsTitle: String public let Username_LinkCopied: String + private let _Time_MonthOfYear_m9: String + private let _Time_MonthOfYear_m9_r: [(Int, NSRange)] + public func Time_MonthOfYear_m9(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m9, self._Time_MonthOfYear_m9_r, [_0]) + } public let DialogList_Conversations: String public let Channel_EditAdmin_PermissionAddAdmins: String public let Conversation_SendMessage: String @@ -656,11 +686,6 @@ public final class PresentationStrings { public func MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_FWDS, self._MESSAGE_FWDS_r, [_1, _2]) } - private let _Time_PreciseDate_5: String - private let _Time_PreciseDate_5_r: [(Int, NSRange)] - public func Time_PreciseDate_5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_5, self._Time_PreciseDate_5_r, [_1, _2, _3]) - } public let Conversation_InputTextCommentPlaceholder: String public let Map_OpenInYandexMaps: String public let Month_ShortNovember: String @@ -704,13 +729,13 @@ public final class PresentationStrings { public let DialogList_DeleteBotConfirmation: String public let Common_TakePhotoOrVideo: String public let Notification_MessageLifetime2s: String - public let Checkout_ErrorGeneric: String public let Conversation_FileGoogleDrive: String private let _MediaPicker_Processing: String private let _MediaPicker_Processing_r: [(Int, NSRange)] public func MediaPicker_Processing(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MediaPicker_Processing, self._MediaPicker_Processing_r, [_0]) } + public let Checkout_ErrorGeneric: String public let Channel_AdminLog_CanBanUsers: String public let Cache_Indexing: String private let _ENCRYPTION_REQUEST: String @@ -725,8 +750,14 @@ public final class PresentationStrings { public let GroupInfo_InviteLink_LinkSection: String public let Privacy_Calls_AlwaysAllow_Placeholder: String public let CheckoutInfo_ShippingInfoPostcode: String + private let _Time_PreciseDate_m7: String + private let _Time_PreciseDate_m7_r: [(Int, NSRange)] + public func Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m7, self._Time_PreciseDate_m7_r, [_1, _2, _3]) + } public let PasscodeSettings_EncryptDataHelp: String public let KeyCommand_FocusOnInputField: String + public let Channel_Members_AddAdminErrorBlacklisted: String public let Cache_KeepMedia: String public let WebPreview_GettingLinkInfo: String public let Group_Setup_TypePublicHelp: String @@ -750,24 +781,24 @@ public final class PresentationStrings { public let Watch_Suggestion_WhatsUp: String public let LoginPassword_PasswordPlaceholder: String public let TwoStepAuth_EnterPasswordPassword: String + private let _Time_PreciseDate_m10: String + private let _Time_PreciseDate_m10_r: [(Int, NSRange)] + public func Time_PreciseDate_m10(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m10, self._Time_PreciseDate_m10_r, [_1, _2, _3]) + } private let _CHANNEL_MESSAGE_CONTACT: String private let _CHANNEL_MESSAGE_CONTACT_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_CONTACT, self._CHANNEL_MESSAGE_CONTACT_r, [_1]) } public let PrivacySettings_DeleteAccountHelp: String - private let _Time_PreciseDate_4: String - private let _Time_PreciseDate_4_r: [(Int, NSRange)] - public func Time_PreciseDate_4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_4, self._Time_PreciseDate_4_r, [_1, _2, _3]) - } public let Channel_Info_Banned: String public let Conversation_ShareBotContactConfirmationTitle: String public let ConversationProfile_UsersTooMuchError: String public let ChatAdmins_AllMembersAreAdminsOffHelp: String public let Privacy_GroupsAndChannels_WhoCanAddMe: String - public let Settings_PhoneNumber: String public let Login_CodeExpiredError: String + public let Settings_PhoneNumber: String private let _DialogList_MultipleTypingSuffix: String private let _DialogList_MultipleTypingSuffix_r: [(Int, NSRange)] public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { @@ -784,6 +815,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Notification_Kicked, self._Notification_Kicked_r, [_0, _1]) } public let Conversation_Send: String + public let Channel_AdminLog_MessageRestrictedForever: String public let ChannelInfo_DeleteChannelConfirmation: String public let Weekday_ShortSaturday: String public let Map_SendThisLocation: String @@ -802,6 +834,7 @@ public final class PresentationStrings { public let PhotoEditor_ContrastTool: String public let MediaPicker_MomentsDateYearFormat: String public let CheckoutInfo_ReceiverInfoNamePlaceholder: String + public let Channel_AdminLog_MessagePreviousCaption: String public let Privacy_PaymentsClear_ShippingInfo: String public let TwoStepAuth_GenericError: String public let Channel_Moderator_AccessLevelEditorHelp: String @@ -809,6 +842,7 @@ public final class PresentationStrings { public let ConversationMedia_EmptyTitle: String public let Date_DialogDateFormat: String public let ReportPeer_ReasonSpam: String + public let Privacy_Calls_P2P: String public let Compose_TokenListPlaceholder: String private let _PINNED_VIDEO: String private let _PINNED_VIDEO_r: [(Int, NSRange)] @@ -830,17 +864,18 @@ public final class PresentationStrings { public let ChangePhoneNumberCode_RequestingACall: String public let PrivacyLastSeenSettings_NeverShareWith_Title: String public let KeyCommand_JumpToNextChat: String + private let _Time_MonthOfYear_m8: String + private let _Time_MonthOfYear_m8_r: [(Int, NSRange)] + public func Time_MonthOfYear_m8(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m8, self._Time_MonthOfYear_m8_r, [_0]) + } public let Tour_Text1: String public let StickerPack_Remove: String public let Conversation_HoldForVideo: String public let Checkout_NewCard_Title: String public let Channel_TitleInfo: String + public let State_ConnectingToProxy: String public let Settings_About_Help: String - private let _Time_PreciseDate_3: String - private let _Time_PreciseDate_3_r: [(Int, NSRange)] - public func Time_PreciseDate_3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_PreciseDate_3, self._Time_PreciseDate_3_r, [_1, _2, _3]) - } public let Watch_Conversation_Reply: String public let ShareMenu_CopyShareLink: String public let Channel_Setup_TypePrivateHelp: String @@ -877,9 +912,10 @@ public final class PresentationStrings { } public let GroupInfo_InvitationLinkDoesNotExist: String public let ReportPeer_ReasonOther_Placeholder: String - public let PasscodeSettings_AutoLock_Disabled: String public let Wallpaper_Title: String + public let PasscodeSettings_AutoLock_Disabled: String public let Watch_Compose_CreateMessage: String + public let ChatSettings_ConnectionType_UseProxy: String public let Message_Audio: String public let Notification_CreatedGroup: String public let Conversation_SearchNoResults: String @@ -903,15 +939,15 @@ public final class PresentationStrings { public let Bot_Unblock: String public let SharedMedia_CategoryMedia: String public let Conversation_HoldForAudio: String - public let Conversation_ClousStorageInfo_Description1: String public let Channel_Members_InviteLink: String - public let WebSearch_RecentClearConfirmation: String public let Core_ServiceUserStatus: String + public let WebSearch_RecentClearConfirmation: String + public let Conversation_ClousStorageInfo_Description1: String public let Notification_ChannelMigratedFrom: String public let Settings_Title: String public let Call_StatusBusy: String - public let ArchivedPacksAlert_Title: String public let ConversationMedia_Title: String + public let ArchivedPacksAlert_Title: String private let _Conversation_MessageViaUser: String private let _Conversation_MessageViaUser_r: [(Int, NSRange)] public func Conversation_MessageViaUser(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -940,6 +976,11 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_BotInteractiveUrlAlert, self._Conversation_BotInteractiveUrlAlert_r, [_0]) } public let GroupInfo_SharedMedia: String + private let _Time_PreciseDate_m6: String + private let _Time_PreciseDate_m6_r: [(Int, NSRange)] + public func Time_PreciseDate_m6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m6, self._Time_PreciseDate_m6_r, [_1, _2, _3]) + } public let Channel_Username_InvalidStartsWithNumber: String public let KeyCommand_JumpToPreviousChat: String public let Conversation_Call: String @@ -993,9 +1034,14 @@ public final class PresentationStrings { public let PasscodeSettings_AutoLock_IfAwayFor_5hours: String public let Notifications_Title: String public let Conversation_PinnedMessage: String - public let Channel_AdminLog_MessagePreviousMessage: String + private let _Time_MonthOfYear_m12: String + private let _Time_MonthOfYear_m12_r: [(Int, NSRange)] + public func Time_MonthOfYear_m12(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m12, self._Time_MonthOfYear_m12_r, [_0]) + } public let ConversationProfile_LeaveDeleteAndExit: String public let State_connecting: String + public let Channel_AdminLog_MessagePreviousMessage: String public let WebPreview_LinkPreview: String public let Map_OpenInHereMaps: String public let CheckoutInfo_Pay: String @@ -1008,8 +1054,8 @@ public final class PresentationStrings { public func CHAT_MESSAGE_AUDIO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_AUDIO, self._CHAT_MESSAGE_AUDIO_r, [_1, _2]) } - public let Login_SmsRequestState2: String public let Preview_SaveToCameraRoll: String + public let Login_SmsRequestState2: String public let PasscodeSettings_ChangePasscode: String public let TwoStepAuth_RecoveryCodeInvalid: String private let _Message_PaymentSent: String @@ -1018,12 +1064,25 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Message_PaymentSent, self._Message_PaymentSent_r, [_0]) } public let Message_PinnedAudioMessage: String + public let ChatSettings_ConnectionType_Title: String + private let _Conversation_RestrictedMediaTimed: String + private let _Conversation_RestrictedMediaTimed_r: [(Int, NSRange)] + public func Conversation_RestrictedMediaTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_RestrictedMediaTimed, self._Conversation_RestrictedMediaTimed_r, [_0]) + } public let Login_InfoDeletePhoto: String + public let Group_Members_AddMemberErrorNotAllowed: String public let Settings_SaveIncomingPhotosHelp: String public let TwoStepAuth_RecoveryCodeExpired: String public let TwoStepAuth_EmailTitle: String public let Privacy_GroupsAndChannels_NeverAllow: String + public let Conversation_RestrictedStickers: String public let Conversation_AddContact: String + private let _Time_MonthOfYear_m7: String + private let _Time_MonthOfYear_m7_r: [(Int, NSRange)] + public func Time_MonthOfYear_m7(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m7, self._Time_MonthOfYear_m7_r, [_0]) + } public let PhotoEditor_QualityLow: String public let Paint_Outlined: String public let Checkout_PasswordEntry_Title: String @@ -1031,8 +1090,8 @@ public final class PresentationStrings { public let PrivacySettings_LastSeenContacts: String public let CheckoutInfo_ShippingInfoAddress1: String public let UserInfo_LastNamePlaceholder: String - public let Conversation_StatusKickedFromChannel: String public let GroupInfo_InviteLink_RevokeAlert_Text: String + public let Conversation_StatusKickedFromChannel: String private let _DialogList_SingleTypingSuffix: String private let _DialogList_SingleTypingSuffix_r: [(Int, NSRange)] public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1051,14 +1110,10 @@ public final class PresentationStrings { public let Settings_About_Title: String public let PhoneNumberHelp_Help: String public let Service_NetworkConfigurationUpdatedMessage: String - private let _Time_MonthOfYear_9: String - private let _Time_MonthOfYear_9_r: [(Int, NSRange)] - public func Time_MonthOfYear_9(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_9, self._Time_MonthOfYear_9_r, [_0]) - } public let Channel_LinkItem: String public let Camera_Retake: String public let StickerPack_ShowStickers: String + public let Conversation_RestrictedText: String private let _CHAT_CREATED: String private let _CHAT_CREATED_r: [(Int, NSRange)] public func CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1096,8 +1151,8 @@ public final class PresentationStrings { public let GroupInfo_DeleteAndExit: String public let GroupInfo_InviteLink_CopyLink: String public let Weekday_Friday: String - public let Login_ResetAccountProtected_Title: String public let Settings_SetProfilePhoto: String + public let Login_ResetAccountProtected_Title: String public let Compose_ChannelTokenListPlaceholder: String public let Channel_EditAdmin_PermissionPinMessages: String public let Your_card_has_expired: String @@ -1116,14 +1171,14 @@ public final class PresentationStrings { } public let KeyCommand_JumpToNextUnreadChat: String public let Conversation_EncryptedDescriptionTitle: String - public let DialogList_Pin: String private let _Notification_RemovedGroupPhoto: String private let _Notification_RemovedGroupPhoto_r: [(Int, NSRange)] public func Notification_RemovedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_RemovedGroupPhoto, self._Notification_RemovedGroupPhoto_r, [_0]) } - public let Channel_ErrorAddTooMuch: String public let GroupInfo_SharedMediaNone: String + public let Channel_ErrorAddTooMuch: String + public let DialogList_Pin: String public let ChatSettings_TextSizeUnits: String public let ChatSettings_AutoPlayAnimations: String public let Conversation_FileOpenIn: String @@ -1136,9 +1191,20 @@ public final class PresentationStrings { public let DialogList_RecentTitleGroups: String public let Privacy_GroupsAndChannels_CustomShareHelp: String public let KeyCommand_ChatInfo: String + public let Channel_AdminLog_EmptyFilterTitle: String public let Notification_CreatedBroadcastList: String public let PhotoEditor_HighlightsTint: String public let Watch_Compose_AddContact: String + private let _Time_PreciseDate_m5: String + private let _Time_PreciseDate_m5_r: [(Int, NSRange)] + public func Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m5, self._Time_PreciseDate_m5_r, [_1, _2, _3]) + } + private let _Channel_AdminLog_MessageKickedNameUsername: String + private let _Channel_AdminLog_MessageKickedNameUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageKickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageKickedNameUsername, self._Channel_AdminLog_MessageKickedNameUsername_r, [_1, _2]) + } public let Coub_TapForSound: String public let Compose_NewEncryptedChat: String public let PhotoEditor_CropReset: String @@ -1155,8 +1221,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Generic_OpenHiddenLinkAlert, self._Generic_OpenHiddenLinkAlert_r, [_0]) } public let Conversation_Contact: String - public let NetworkUsageSettings_GeneralDataSection: String public let Service_ApplyLocalization: String + public let NetworkUsageSettings_GeneralDataSection: String private let _StickerPack_RemovePrompt: String private let _StickerPack_RemovePrompt_r: [(Int, NSRange)] public func StickerPack_RemovePrompt(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1174,16 +1240,9 @@ public final class PresentationStrings { public let Conversation_ContextMenuDelete: String public let Tour_Text6: String public let PhotoEditor_WarmthTool: String - private let _Time_MonthOfYear_8: String - private let _Time_MonthOfYear_8_r: [(Int, NSRange)] - public func Time_MonthOfYear_8(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_8, self._Time_MonthOfYear_8_r, [_0]) - } public let Common_TakePhoto: String public let PhotoEditor_Current: String public let UserInfo_CreateNewContact: String - public let NetworkUsageSettings_MediaDocumentDataSection: String - public let Login_CodeSentCall: String public let Watch_PhotoView_Title: String private let _PrivacySettings_LastSeenContactsMinus: String private let _PrivacySettings_LastSeenContactsMinus_r: [(Int, NSRange)] @@ -1191,7 +1250,9 @@ public final class PresentationStrings { return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsMinus, self._PrivacySettings_LastSeenContactsMinus_r, [_0]) } public let Login_InfoUpdatePhoto: String + public let Login_CodeSentCall: String public let ShareMenu_SelectChats: String + public let NetworkUsageSettings_MediaDocumentDataSection: String public let Group_ErrorSendRestrictedMedia: String public let Channel_EditAdmin_PermissinAddAdminOff: String public let Cache_Files: String @@ -1236,6 +1297,7 @@ public final class PresentationStrings { public let Weekday_Yesterday: String public let Conversation_InputTextSilentBroadcastPlaceholder: String public let Embed_PlayingInPIP: String + public let Localization_EnglishLanguageName: String public let Call_StatusIncoming: String public let Conversation_Play: String public let Settings_PrivacySettings: String @@ -1253,6 +1315,11 @@ public final class PresentationStrings { public let Compose_NewChannel_AddMemberHelp: String public let GroupInfo_ChatAdmins: String public let PhotoEditor_CurvesAll: String + private let _Notification_LeftChannel: String + private let _Notification_LeftChannel_r: [(Int, NSRange)] + public func Notification_LeftChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_LeftChannel, self._Notification_LeftChannel_r, [_0]) + } public let Compose_Create: String private let _LOCKED_MESSAGE: String private let _LOCKED_MESSAGE_r: [(Int, NSRange)] @@ -1260,6 +1327,11 @@ public final class PresentationStrings { return formatWithArgumentRanges(_LOCKED_MESSAGE, self._LOCKED_MESSAGE_r, [_1]) } public let Conversation_ContextMenuShare: String + private let _Time_MonthOfYear_m6: String + private let _Time_MonthOfYear_m6_r: [(Int, NSRange)] + public func Time_MonthOfYear_m6(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m6, self._Time_MonthOfYear_m6_r, [_0]) + } private let _Call_GroupFormat: String private let _Call_GroupFormat_r: [(Int, NSRange)] public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1273,15 +1345,15 @@ public final class PresentationStrings { public func CHAT_JOINED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_JOINED, self._CHAT_JOINED_r, [_1, _2]) } + private let _Channel_AdminLog_MessageInvitedName: String + private let _Channel_AdminLog_MessageInvitedName_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageInvitedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageInvitedName, self._Channel_AdminLog_MessageInvitedName_r, [_1]) + } public let Conversation_Moderate_Ban: String public let Group_Status: String public let Watch_Suggestion_Absolutely: String public let Conversation_InputTextPlaceholder: String - private let _Time_MonthOfYear_7: String - private let _Time_MonthOfYear_7_r: [(Int, NSRange)] - public func Time_MonthOfYear_7(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_7, self._Time_MonthOfYear_7_r, [_0]) - } public let SharedMedia_TitleAudio: String public let TwoStepAuth_RecoveryCode: String public let SharedMedia_CategoryDocs: String @@ -1348,6 +1420,11 @@ public final class PresentationStrings { public func Message_ForwardedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Message_ForwardedMessage, self._Message_ForwardedMessage_r, [_0]) } + private let _Time_PreciseDate_m4: String + private let _Time_PreciseDate_m4_r: [(Int, NSRange)] + public func Time_PreciseDate_m4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m4, self._Time_PreciseDate_m4_r, [_1, _2, _3]) + } public let Checkout_NewCard_SaveInfoEnableHelp: String public let Call_AudioRouteHide: String public let CallSettings_OnMobile: String @@ -1355,6 +1432,7 @@ public final class PresentationStrings { public let CheckoutInfo_ErrorCityInvalid: String public let Profile_CreateEncryptedChatError: String public let Map_LocationTitle: String + public let Call_RateCall: String public let Compose_Recipients: String public let Message_ReplyActionButtonShowReceipt: String public let PhotoEditor_ShadowsTool: String @@ -1383,11 +1461,6 @@ public final class PresentationStrings { public let Cache_ClearCacheAlert: String public let BroadcastLists_NoListsYet: String public let Settings_TabTitle: String - private let _Time_MonthOfYear_6: String - private let _Time_MonthOfYear_6_r: [(Int, NSRange)] - public func Time_MonthOfYear_6(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_6, self._Time_MonthOfYear_6_r, [_0]) - } public let NetworkUsageSettings_MediaAudioDataSection: String public let GroupInfo_DeactivatedStatus: String private let _CHAT_PHOTO_EDITED: String @@ -1402,6 +1475,11 @@ public final class PresentationStrings { return formatWithArgumentRanges(_PrivacySettings_LastSeenEverybodyMinus, self._PrivacySettings_LastSeenEverybodyMinus_r, [_0]) } public let Weekday_Today: String + private let _Conversation_RestrictedStickersTimed: String + private let _Conversation_RestrictedStickersTimed_r: [(Int, NSRange)] + public func Conversation_RestrictedStickersTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_RestrictedStickersTimed, self._Conversation_RestrictedStickersTimed_r, [_0]) + } public let Login_InvalidFirstNameError: String private let _Notification_Joined: String private let _Notification_Joined_r: [(Int, NSRange)] @@ -1413,8 +1491,8 @@ public final class PresentationStrings { public func VideoPreview_OptionHD(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_VideoPreview_OptionHD, self._VideoPreview_OptionHD_r, [_0]) } - public let Paint_Clear: String public let TwoStepAuth_RecoveryFailed: String + public let Paint_Clear: String private let _MESSAGE_AUDIO: String private let _MESSAGE_AUDIO_r: [(Int, NSRange)] public func MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -1457,6 +1535,11 @@ public final class PresentationStrings { public func Notification_PinnedDeletedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_PinnedDeletedMessage, self._Notification_PinnedDeletedMessage_r, [_0]) } + private let _Time_MonthOfYear_m11: String + private let _Time_MonthOfYear_m11_r: [(Int, NSRange)] + public func Time_MonthOfYear_m11(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m11, self._Time_MonthOfYear_m11_r, [_0]) + } public let UserInfo_BotHelp: String public let Contacts_contact: String public let TwoStepAuth_PasswordSet: String @@ -1494,11 +1577,6 @@ public final class PresentationStrings { public let GroupInfo_InviteLink_RevokeLink: String public let Conversation_Unmute: String public let Checkout_PaymentMethod_Title: String - private let _AppLanguage_LanguageSuggested: String - private let _AppLanguage_LanguageSuggested_r: [(Int, NSRange)] - public func AppLanguage_LanguageSuggested(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_AppLanguage_LanguageSuggested, self._AppLanguage_LanguageSuggested_r, [_0]) - } public let Notifications_MessageNotifications: String public let ChannelMembers_WhoCanAddMembersAdminsHelp: String public let DialogList_DeleteBotConversationConfirmation: String @@ -1513,6 +1591,11 @@ public final class PresentationStrings { return formatWithArgumentRanges(_GroupInfo_InvitationLinkAccept, self._GroupInfo_InvitationLinkAccept_r, [_0]) } public let Conversation_ClousStorageInfo_Description2: String + private let _Time_MonthOfYear_m5: String + private let _Time_MonthOfYear_m5_r: [(Int, NSRange)] + public func Time_MonthOfYear_m5(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m5, self._Time_MonthOfYear_m5_r, [_0]) + } public let Map_Hybrid: String public let Channel_Setup_Title: String public let Activity_UploadingVideo: String @@ -1540,23 +1623,21 @@ public final class PresentationStrings { public let Channel_NotificationCommentsEnabled: String public let PasscodeSettings_UnlockWithTouchId: String public let Contacts_AccessDeniedHelpON: String - private let _Time_MonthOfYear_5: String - private let _Time_MonthOfYear_5_r: [(Int, NSRange)] - public func Time_MonthOfYear_5(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_5, self._Time_MonthOfYear_5_r, [_0]) - } + public let NetworkUsageSettings_ResetStats: String private let _PrivacySettings_LastSeenContactsMinusPlus: String private let _PrivacySettings_LastSeenContactsMinusPlus_r: [(Int, NSRange)] public func PrivacySettings_LastSeenContactsMinusPlus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsMinusPlus, self._PrivacySettings_LastSeenContactsMinusPlus_r, [_0, _1]) } - public let NetworkUsageSettings_ResetStats: String + public let Channel_AdminLog_EmptyMessageText: String private let _Notification_ChannelInviter: String private let _Notification_ChannelInviter_r: [(Int, NSRange)] public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_ChannelInviter, self._Notification_ChannelInviter_r, [_0]) } + public let SocksProxySetup_TypeSocks: String public let Profile_MessageLifetimeForever: String + public let SocksProxySetup_Username: String public let Conversation_Edit: String public let TwoStepAuth_ResetAccountHelp: String public let Month_GenDecember: String @@ -1578,6 +1659,7 @@ public final class PresentationStrings { } public let Channel_AdminLogFilter_EventsEditedMessages: String public let Channel_Username_InvalidTooShort: String + public let Conversation_ViewGroup: String public let Watch_LastSeen_WithinAWeek: String public let BlockedUsers_SelectUserTitle: String public let Profile_MessageLifetime1w: String @@ -1589,40 +1671,56 @@ public final class PresentationStrings { public func Conversation_DownloadKilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_DownloadKilobytes, self._Conversation_DownloadKilobytes_r, ["\(_0)"]) } + private let _Channel_AdminLog_MessagePromotedName: String + private let _Channel_AdminLog_MessagePromotedName_r: [(Int, NSRange)] + public func Channel_AdminLog_MessagePromotedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessagePromotedName, self._Channel_AdminLog_MessagePromotedName_r, [_1]) + } private let _Username_LinkHint: String private let _Username_LinkHint_r: [(Int, NSRange)] public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Username_LinkHint, self._Username_LinkHint_r, [_0]) } + public let Group_Members_AddMemberBotErrorNotAllowed: String public let NetworkUsageSettings_Title: String public let CheckoutInfo_ShippingInfoPostcodePlaceholder: String public let Wallpaper_Wallpaper: String public let GroupInfo_InviteLink_RevokeAlert_Revoke: String public let SharedMedia_TitleLink: String + private let _Channel_AdminLog_MessageRestrictedName: String + private let _Channel_AdminLog_MessageRestrictedName_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRestrictedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedName, self._Channel_AdminLog_MessageRestrictedName_r, [_1]) + } public let Channel_JoinChannel: String - public let StickerPack_Add: String - public let Group_ErrorNotMutualContact: String public let AccessDenied_LocationDisabled: String + public let Group_ErrorNotMutualContact: String public let Conversation_DownloadPhoto: String - public let Login_UnknownError: String public let Presence_online: String + public let Login_UnknownError: String public let DialogList_Title: String - public let Stickers_Install: String public let SearchImages_NoImagesFound: String private let _Notification_RemovedUserPhoto: String private let _Notification_RemovedUserPhoto_r: [(Int, NSRange)] public func Notification_RemovedUserPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_RemovedUserPhoto, self._Notification_RemovedUserPhoto_r, [_0]) } + public let Stickers_Install: String private let _Watch_Time_ShortTodayAt: String private let _Watch_Time_ShortTodayAt_r: [(Int, NSRange)] public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Watch_Time_ShortTodayAt, self._Watch_Time_ShortTodayAt_r, [_0]) } - public let UserInfo_GroupsInCommon: String + public let StickerPack_Add: String public let ChatSettings_Language: String - public let AccessDenied_CameraDisabled: String public let Message_PinnedContactMessage: String + public let AccessDenied_CameraDisabled: String + private let _Time_PreciseDate_m3: String + private let _Time_PreciseDate_m3_r: [(Int, NSRange)] + public func Time_PreciseDate_m3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m3, self._Time_PreciseDate_m3_r, [_1, _2, _3]) + } + public let UserInfo_GroupsInCommon: String public let UserInfo_Call: String public let Conversation_InputTextDisabledPlaceholder: String public let Map_ForwardViaTelegram: String @@ -1662,11 +1760,6 @@ public final class PresentationStrings { public let Message_Photo: String public let Conversation_ReportSpam: String public let Camera_FlashAuto: String - private let _Time_MonthOfYear_4: String - private let _Time_MonthOfYear_4_r: [(Int, NSRange)] - public func Time_MonthOfYear_4(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_4, self._Time_MonthOfYear_4_r, [_0]) - } public let Call_ConnectionErrorMessage: String public let Compose_NewChannel_AddMember: String public let Watch_State_Updating: String @@ -1678,6 +1771,11 @@ public final class PresentationStrings { public let Watch_Suggestion_OnMyWay: String public let Checkout_NewCard_PaymentCard: String public let PhotoEditor_CropAspectRatioOriginal: String + private let _Conversation_RestrictedInlineTimed: String + private let _Conversation_RestrictedInlineTimed_r: [(Int, NSRange)] + public func Conversation_RestrictedInlineTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_RestrictedInlineTimed, self._Conversation_RestrictedInlineTimed_r, [_0]) + } public let MediaPicker_MomentsDateRangeFormat: String public let UserInfo_NotificationsDisabled: String private let _CONTACT_JOINED: String @@ -1689,6 +1787,11 @@ public final class PresentationStrings { public let BlockedUsers_LeavePrefix: String public let NetworkUsageSettings_ResetStatsConfirmation: String public let Channel_EditAdmin_PermissionPostMessages: String + private let _Contacts_AddPhoneNumber: String + private let _Contacts_AddPhoneNumber_r: [(Int, NSRange)] + public func Contacts_AddPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Contacts_AddPhoneNumber, self._Contacts_AddPhoneNumber_r, [_0]) + } public let DialogList_EncryptionProcessing: String public let Conversation_ApplyLocalization: String public let Conversation_DeleteManyMessages: String @@ -1750,9 +1853,19 @@ public final class PresentationStrings { public func PrivacySettings_LastSeenNobodyPlus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PrivacySettings_LastSeenNobodyPlus, self._PrivacySettings_LastSeenNobodyPlus_r, [_0]) } + private let _Time_MonthOfYear_m4: String + private let _Time_MonthOfYear_m4_r: [(Int, NSRange)] + public func Time_MonthOfYear_m4(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m4, self._Time_MonthOfYear_m4_r, [_0]) + } public let Preview_ForwardViaTelegram: String public let Notifications_InAppNotificationsSounds: String public let Call_StatusRequesting: String + private let _Channel_AdminLog_MessageRestrictedUntil: String + private let _Channel_AdminLog_MessageRestrictedUntil_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedUntil, self._Channel_AdminLog_MessageRestrictedUntil_r, [_0]) + } private let _CHAT_MESSAGE_CONTACT: String private let _CHAT_MESSAGE_CONTACT_r: [(Int, NSRange)] public func CHAT_MESSAGE_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1777,13 +1890,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_Bytes, self._Conversation_Bytes_r, ["\(_0)"]) } public let GroupInfo_InviteLink_Help: String - private let _Time_MonthOfYear_3: String - private let _Time_MonthOfYear_3_r: [(Int, NSRange)] - public func Time_MonthOfYear_3(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Time_MonthOfYear_3, self._Time_MonthOfYear_3_r, [_0]) - } - public let Conversation_ContextMenuForward: String public let Calls_Missed: String + public let Conversation_ContextMenuForward: String public let Call_StatusRinging: String public let Invitation_JoinGroup: String public let Notification_PinnedMessage: String @@ -1817,9 +1925,11 @@ public final class PresentationStrings { } public let GroupInfo_BroadcastListNamePlaceholder: String public let Conversation_ShareBotContactConfirmation: String + public let GroupInfo_ActionBan: String public let Login_CodeSentSms: String public let Conversation_ReportSpamConfirmation: String public let ChannelMembers_ChannelAdminsTitle: String + public let SocksProxySetup_Credentials: String public let CallSettings_UseLessData: String private let _TwoStepAuth_EnterPasswordHint: String private let _TwoStepAuth_EnterPasswordHint_r: [(Int, NSRange)] @@ -1857,18 +1967,23 @@ public final class PresentationStrings { return formatWithArgumentRanges(_SearchImages_ImageNofM, self._SearchImages_ImageNofM_r, [_1, _2]) } public let Channel_Username_CreatePrivateLinkHelp: String + private let _Time_PreciseDate_m2: String + private let _Time_PreciseDate_m2_r: [(Int, NSRange)] + public func Time_PreciseDate_m2(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m2, self._Time_PreciseDate_m2_r, [_1, _2, _3]) + } private let _FileSize_B: String private let _FileSize_B_r: [(Int, NSRange)] public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_FileSize_B, self._FileSize_B_r, [_0]) } + public let PhotoEditor_SaturationTool: String + public let ImagePicker_NoPhotos: String private let _Target_ShareGameConfirmationGroup: String private let _Target_ShareGameConfirmationGroup_r: [(Int, NSRange)] public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Target_ShareGameConfirmationGroup, self._Target_ShareGameConfirmationGroup_r, [_0]) } - public let PhotoEditor_SaturationTool: String - public let ImagePicker_NoPhotos: String public let Call_StatusConnecting: String public let Channel_BanUser_BlockFor: String public let Preview_DeleteVideo: String @@ -1939,6 +2054,7 @@ public final class PresentationStrings { public let Channel_About_Help: String public let Web_OpenExternal: String public let UserInfo_AddContact: String + public let SocksProxySetup_Connection: String public let Call_EncryptionKey_Title: String public let PhotoEditor_BlurToolLinear: String public let AuthSessions_EmptyText: String @@ -1963,34 +2079,51 @@ public final class PresentationStrings { public let TwoStepAuth_RecoveryTitle: String public let WatchRemote_AlertOpen: String public let ExplicitContent_AlertChannel: String - public let TwoStepAuth_ConfirmationText: String - public let Widget_AuthRequired: String private let _ForwardedAuthors2: String private let _ForwardedAuthors2_r: [(Int, NSRange)] public func ForwardedAuthors2(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ForwardedAuthors2, self._ForwardedAuthors2_r, [_0, _1]) } + public let TwoStepAuth_ConfirmationText: String public let ChannelInfo_DeleteGroupConfirmation: String public let Login_SmsRequestState3: String public let Notifications_AlertTones: String - public let Calls_TabTitle: String + private let _Time_MonthOfYear_m10: String + private let _Time_MonthOfYear_m10_r: [(Int, NSRange)] + public func Time_MonthOfYear_m10(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m10, self._Time_MonthOfYear_m10_r, [_0]) + } public let Login_InfoAvatarPhoto: String + public let Widget_AuthRequired: String + public let Calls_TabTitle: String public let Contacts_MemberSearchSectionTitleChannel: String public let PhotoEditor_CurvesTool: String public let Preview_LoadingVideo: String public let State_updating: String + private let _Notification_JoinedChannel: String + private let _Notification_JoinedChannel_r: [(Int, NSRange)] + public func Notification_JoinedChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_JoinedChannel, self._Notification_JoinedChannel_r, [_0]) + } public let TwoStepAuth_ResetAccount: String + public let GroupInfo_ActionRestrict: String public let Checkout_ShippingOption_Title: String public let Weekday_Tuesday: String public let Preview_Tooltip: String public let Conversation_EncryptionProcessing: String + public let Weekday_ShortSunday: String private let _CHAT_ADD_MEMBER: String private let _CHAT_ADD_MEMBER_r: [(Int, NSRange)] public func CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_ADD_MEMBER, self._CHAT_ADD_MEMBER_r, [_1, _2, _3]) } - public let Weekday_ShortSunday: String + private let _Channel_AdminLog_MessageKickedName: String + private let _Channel_AdminLog_MessageKickedName_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageKickedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageKickedName, self._Channel_AdminLog_MessageKickedName_r, [_1]) + } public let Month_ShortJune: String + public let Privacy_Calls_Integration: String public let Month_GenApril: String public let StickerPacksSettings_ShowStickersButton: String public let MediaPicker_MomentsDateRangeSameMonthFormat: String @@ -2004,15 +2137,21 @@ public final class PresentationStrings { public let CallSettings_RecentCalls: String public let Conversation_Megabytes: String public let TwoStepAuth_FloodError: String - public let Paint_Stickers: String public let Login_InvalidCountryCode: String + public let Paint_Stickers: String public let Privacy_Calls_AlwaysAllow_Title: String public let Username_InvalidTooShort: String public let Weekday_ShortFriday: String public let Conversation_ClearAll: String public let MediaPicker_Moments: String - public let Call_PhoneCallInProgressMessage: String + public let Call_ReportIncludeLog: String + private let _Time_MonthOfYear_m3: String + private let _Time_MonthOfYear_m3_r: [(Int, NSRange)] + public func Time_MonthOfYear_m3(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m3, self._Time_MonthOfYear_m3_r, [_0]) + } public let SharedMedia_EmptyTitle: String + public let Call_PhoneCallInProgressMessage: String public let Checkout_Name: String public let Preview_GroupPhotoTitle: String private let _AUTH_REGION: String @@ -2028,23 +2167,28 @@ public final class PresentationStrings { } public let Conversation_EncryptionCanceled: String public let AccessDenied_SaveMedia: String + private let _Channel_AdminLog_MessageInvitedNameUsername: String + private let _Channel_AdminLog_MessageInvitedNameUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageInvitedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageInvitedNameUsername, self._Channel_AdminLog_MessageInvitedNameUsername_r, [_1, _2]) + } public let Channel_Username_InvalidTooManyUsernames: String public let Compose_GroupTokenListPlaceholder: String public let Profile_ImageUploadError: String public let Conversation_MessageDeliveryFailed: String public let Privacy_PaymentsClear_PaymentInfo: String - public let Notification_Mute1hMin: String public let Notifications_GroupNotifications: String + public let Notification_Mute1hMin: String public let CheckoutInfo_SaveInfoHelp: String public let StickerPacksSettings_ArchivedMasks_Info: String public let ChannelMembers_WhoCanAddMembers_AllMembers: String public let Channel_Edit_PrivatePublicLinkAlert: String public let Watch_Conversation_UserInfo: String - public let Application_Name: String - public let Conversation_AddToReadingList: String public let Conversation_FileDropbox: String public let Login_PhonePlaceholder: String public let ExplicitContent_AlertUser: String + public let Conversation_AddToReadingList: String + public let Application_Name: String public let Profile_MessageLifetime1d: String public let Calls_CallTabDescription: String public let CheckoutInfo_ShippingInfoCityPlaceholder: String @@ -2054,6 +2198,7 @@ public final class PresentationStrings { public let Channel_Setup_TypePublicHelp: String public let GroupInfo_InviteLink_RevokeAlert_Success: String public let Channel_Setup_PublicNoLink: String + public let Privacy_Calls_P2PHelp: String public let Conversation_Info: String public let ChannelInfo_InvitationLinkDoesNotExist: String private let _Time_TodayAt: String @@ -2062,11 +2207,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_TodayAt, self._Time_TodayAt_r, [_0]) } public let Conversation_Processing: String - private let _InstantPage_AuthorAndDateTitle: String - private let _InstantPage_AuthorAndDateTitle_r: [(Int, NSRange)] - public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_InstantPage_AuthorAndDateTitle, self._InstantPage_AuthorAndDateTitle_r, [_1, _2]) - } + public let Conversation_RestrictedInline: String private let _Watch_LastSeen_AtDate: String private let _Watch_LastSeen_AtDate_r: [(Int, NSRange)] public func Watch_LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2074,6 +2215,11 @@ public final class PresentationStrings { } public let Conversation_Location: String public let DialogList_PasscodeLockHelp: String + private let _InstantPage_AuthorAndDateTitle: String + private let _InstantPage_AuthorAndDateTitle_r: [(Int, NSRange)] + public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_InstantPage_AuthorAndDateTitle, self._InstantPage_AuthorAndDateTitle_r, [_1, _2]) + } public let Channel_Management_Title: String public let Notifications_InAppNotificationsPreview: String public let PrivacySettings_FloodControlError: String @@ -2092,8 +2238,8 @@ public final class PresentationStrings { public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_FileSize_MB, self._FileSize_MB_r, [_0]) } - public let ChatSearch_SearchPlaceholder: String public let TwoStepAuth_ConfirmationAbort: String + public let ChatSearch_SearchPlaceholder: String public let GroupInfo_KickedStatus: String public let TwoStepAuth_SetupPasswordConfirmFailed: String private let _LastSeen_YesterdayAt: String @@ -2105,6 +2251,7 @@ public final class PresentationStrings { public let Localization_LanguageName: String public let Map_OpenIn: String public let Message_File: String + public let Call_ReportSend: String private let _Channel_AdminLog_MessageChangedGroupUsername: String private let _Channel_AdminLog_MessageChangedGroupUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageChangedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2115,6 +2262,11 @@ public final class PresentationStrings { public func CHAT_MESSAGE_GAME(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_GAME, self._CHAT_MESSAGE_GAME_r, [_1, _2, _3]) } + private let _Time_PreciseDate_m1: String + private let _Time_PreciseDate_m1_r: [(Int, NSRange)] + public func Time_PreciseDate_m1(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m1, self._Time_PreciseDate_m1_r, [_1, _2, _3]) + } public let Month_ShortMay: String private let _WelcomeScreen_Greeting: String private let _WelcomeScreen_Greeting_r: [(Int, NSRange)] @@ -2157,12 +2309,12 @@ public final class PresentationStrings { public let EnterPasscode_EnterPasscode: String public let Notifications_Reset: String public let GroupInfo_InvitationLinkGroupFull: String + public let GoogleDrive_LogoutLogout: String private let _Channel_AdminLog_MessageChangedChannelUsername: String private let _Channel_AdminLog_MessageChangedChannelUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedChannelUsername, self._Channel_AdminLog_MessageChangedChannelUsername_r, [_0]) } - public let GoogleDrive_LogoutLogout: String private let _CHAT_MESSAGE_DOC: String private let _CHAT_MESSAGE_DOC_r: [(Int, NSRange)] public func CHAT_MESSAGE_DOC(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -2221,13 +2373,13 @@ public final class PresentationStrings { public func Contacts_AccessDeniedHelpPortrait(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Contacts_AccessDeniedHelpPortrait, self._Contacts_AccessDeniedHelpPortrait_r, [_0]) } - public let Channel_Info_BlackList: String private let _Checkout_LiabilityAlert: String private let _Checkout_LiabilityAlert_r: [(Int, NSRange)] - public func Checkout_LiabilityAlert(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1]) + public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1, _1, _2]) } public let Profile_BotInfo: String + public let Channel_Info_BlackList: String public let StickerPack_RemoveStickers: String public let Compose_NewChannel_Members: String public let Notification_Reply: String @@ -2256,6 +2408,11 @@ public final class PresentationStrings { public let Message_PinnedAnimationMessage: String public let Checkout_ErrorPrecheckoutFailed: String public let Camera_PhotoMode: String + private let _Time_MonthOfYear_m2: String + private let _Time_MonthOfYear_m2_r: [(Int, NSRange)] + public func Time_MonthOfYear_m2(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m2, self._Time_MonthOfYear_m2_r, [_0]) + } public let Channel_About_Placeholder: String public let Channel_About_Title: String private let _MESSAGE_PHOTO: String @@ -2307,6 +2464,11 @@ public final class PresentationStrings { public let Channel_AdminLog_BanSendStickers: String public let Common_Next: String public let Watch_Notification_Joined: String + private let _Channel_AdminLog_MessageRestrictedNewSetting: String + private let _Channel_AdminLog_MessageRestrictedNewSetting_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRestrictedNewSetting(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedNewSetting, self._Channel_AdminLog_MessageRestrictedNewSetting_r, [_0]) + } public let ImagePicker_NoVideos: String public let GroupInfo_DeleteAndExitConfirmation: String public let ChatSettings_Cache: String @@ -2343,6 +2505,7 @@ public final class PresentationStrings { public let DialogList_RecentTitlePeople: String public let Conversation_ViewLocation: String public let GroupInfo_Notifications: String + public let Call_ReportPlaceholder: String private let _MESSAGE_DOC: String private let _MESSAGE_DOC_r: [(Int, NSRange)] public func MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2381,6 +2544,7 @@ public final class PresentationStrings { public let Conversation_MessageDialogRetry: String public let Watch_ChatList_NoConversationsTitle: String public let BlockedUsers_Title: String + public let ChatSettings_ConnectionType_UseSocks5: String public let MediaPicker_MomentsDateRangeYearFormat: String public let Cache_ClearNone: String public let Login_InvalidCodeError: String @@ -2396,6 +2560,8 @@ public final class PresentationStrings { public let BlockedUsers_AlreadyBlocked: String public let PrivacySettings_DeleteAccountIfAwayFor: String public let PrivacySettings_DeleteAccountTitle: String + public let Channel_AdminLog_EmptyText: String + public let Channel_AdminLog_EmptyFilterText: String public let PrivacyLastSeenSettings_CustomShareSettings_Delete: String private let _ENCRYPTED_MESSAGE: String private let _ENCRYPTED_MESSAGE_r: [(Int, NSRange)] @@ -2421,17 +2587,19 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHANNEL_MESSAGE_ROUND, self._CHANNEL_MESSAGE_ROUND_r, [_1]) } public let GoogleDrive_FolderLoadError: String + public let SocksProxySetup_Port: String public let Message_VideoMessage: String public let Conversation_ContextMenuStickerPackInfo: String - public let Login_ResetAccountProtected_LimitExceeded: String public let Watch_Suggestion_TextInABit: String private let _CHAT_DELETE_MEMBER: String private let _CHAT_DELETE_MEMBER_r: [(Int, NSRange)] public func CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_DELETE_MEMBER, self._CHAT_DELETE_MEMBER_r, [_1, _2, _3]) } + public let Login_ResetAccountProtected_LimitExceeded: String public let Conversation_EncryptedForwardingAlert: String public let Conversation_DiscardVoiceMessageAction: String + public let Camera_Title: String public let PhotoEditor_CurvesBlue: String public let Message_PinnedVideoMessage: String private let _Settings_OpenSystemPrivacySettings: String @@ -2454,8 +2622,9 @@ public final class PresentationStrings { } public let Map_Unknown: String public let Wallpaper_Set: String - public let SharedMedia_CategoryLinks: String public let AccessDenied_Title: String + public let SharedMedia_CategoryLinks: String + public let Localization_LanguageOther: String public let Conversation_ClearAllConfirmation: String public let TwoStepAuth_EmailSkipAlert: String public let ChatSettings_Stickers: String @@ -2525,9 +2694,15 @@ public final class PresentationStrings { public func PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_GIF, self._PINNED_GIF_r, [_1]) } + public let Channel_EditAdmin_CannotEdit: String public let Profile_PhonebookAccessDisabled: String public let LoginPassword_PasswordHelp: String public let BlockedUsers_Unblock: String + private let _Time_MonthOfYear_m1: String + private let _Time_MonthOfYear_m1_r: [(Int, NSRange)] + public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_m1, self._Time_MonthOfYear_m1_r, [_0]) + } public let Conversation_ViewFile: String public let Notifications_GroupNotificationsAlert: String public let Paint_Masks: String @@ -2548,6 +2723,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_FileSize_KB, self._FileSize_KB_r, [_0]) } public let Watch_GroupInfo_Title: String + public let Channel_AdminLog_EmptyTitle: String public let PhotoEditor_Set: String private let _Notification_Invited: String private let _Notification_Invited_r: [(Int, NSRange)] @@ -2557,9 +2733,10 @@ public final class PresentationStrings { public let Watch_AuthRequired: String public let Conversation_EncryptedDescription1: String public let AppleWatch_ReplyPresets: String + public let Channel_Members_AddAdminErrorNotAMember: String public let Conversation_EncryptedDescription2: String - public let NetworkUsageSettings_MediaVideoDataSection: String public let Paint_Edit: String + public let NetworkUsageSettings_MediaVideoDataSection: String public let Conversation_EncryptedDescription3: String public let Login_CodeFloodError: String private let _Call_EncryptionKey_Description: String @@ -2569,8 +2746,8 @@ public final class PresentationStrings { } public let Conversation_EncryptedDescription4: String public let AppleWatch_Title: String - public let Conversation_StatusTyping: String public let Contacts_AccessDeniedError: String + public let Conversation_StatusTyping: String public let GoogleDrive_LoadErrorTitle: String public let Share_Title: String public let Map_Send: String @@ -2606,6 +2783,7 @@ public final class PresentationStrings { public func Profile_ShareBotGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Profile_ShareBotGroupFormat, self._Profile_ShareBotGroupFormat_r, [_0]) } + public let Call_ReportIncludeLogDescription: String public let Preview_DeleteGif: String public let Weekday_Saturday: String public let UserInfo_DeleteContact: String @@ -2626,6 +2804,11 @@ public final class PresentationStrings { public let Channel_AdminLog_CanPinMessages: String public let KeyCommand_NewMessage: String public let Compose_NewBroadcastButton: String + private let _Time_PreciseDate_m12: String + private let _Time_PreciseDate_m12_r: [(Int, NSRange)] + public func Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_m12, self._Time_PreciseDate_m12_r, [_1, _2, _3]) + } public let NetworkUsageSettings_TotalSection: String private let _PINNED_AUDIO: String private let _PINNED_AUDIO_r: [(Int, NSRange)] @@ -2682,6 +2865,7 @@ public final class PresentationStrings { public let Notifications_GroupNotificationsHelp: String public let PhotoEditor_CropAspectRatioSquare: String public let Notification_CallOutgoing: String + public let SocksProxySetup_Password: String public let Weekday_ShortMonday: String public let Channel_Edit_AboutItem: String public let Checkout_Receipt_Title: String @@ -2717,20 +2901,22 @@ public final class PresentationStrings { public func CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHANNEL_MESSAGE_GEO, self._CHANNEL_MESSAGE_GEO_r, [_1]) } + public let Contacts_PhoneNumber: String public let Group_Info_AdminLog: String + public let Channel_AdminLogFilter_ChannelEventsInfo: String public let StickerPacksSettings_FeaturedPacks: String public let Month_GenAugust: String public let Channel_Username_CreatePublicLinkHelp: String public let StickerPack_Send: String public let Watch_Suggestion_HoldOn: String - public let StickerSettings_MaskContextInfo: String public let AttachmentMenu_ImageSearch: String + public let PasscodeSettings_EncryptData: String private let _PINNED_GEO: String private let _PINNED_GEO_r: [(Int, NSRange)] public func PINNED_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_GEO, self._PINNED_GEO_r, [_1]) } - public let PasscodeSettings_EncryptData: String + public let StickerSettings_MaskContextInfo: String public let Notification_CallCanceled: String public let Common_NotNow: String public let PasscodeSettings_Title: String @@ -2749,6 +2935,7 @@ public final class PresentationStrings { public let Channel_EditAdmin_PermissionBanUsers: String public let Wallpaper_PhotoLibrary: String public let Settings_About: String + public let Privacy_Calls_IntegrationHelp: String private let _CHAT_LEFT: String private let _CHAT_LEFT_r: [(Int, NSRange)] public func CHAT_LEFT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -2790,7 +2977,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_MessageTitleUpdated, self._Channel_MessageTitleUpdated_r, [_0]) } public let Call_CallAgain: String + public let Message_VideoExpired: String public let TwoStepAuth_RecoveryCodeHelp: String + private let _Channel_AdminLog_MessagePromotedNameUsername: String + private let _Channel_AdminLog_MessagePromotedNameUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessagePromotedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessagePromotedNameUsername, self._Channel_AdminLog_MessagePromotedNameUsername_r, [_1, _2]) + } public let UserInfo_SendMessage: String private let _Channel_Username_LinkHint: String private let _Channel_Username_LinkHint_r: [(Int, NSRange)] @@ -2806,17 +2999,17 @@ public final class PresentationStrings { public let Channel_Moderator_Title: String public let Message_PinnedPhotoMessage: String public let Notification_SecretChatScreenshot: String + public let Activity_UploadingDocument: String + public let AccessDenied_LocationTracking: String + public let Watch_ChatList_NoConversationsText: String + public let ReportPeer_AlertSuccess: String + public let Tour_Text4: String + public let Channel_Info_Description: String private let _Conversation_DeleteMessagesFor: String private let _Conversation_DeleteMessagesFor_r: [(Int, NSRange)] public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_DeleteMessagesFor, self._Conversation_DeleteMessagesFor_r, [_0]) } - public let Activity_UploadingDocument: String - public let Watch_ChatList_NoConversationsText: String - public let ReportPeer_AlertSuccess: String - public let Tour_Text4: String - public let Channel_Info_Description: String - public let AccessDenied_LocationTracking: String public let MessageTimer_Title: String public let Watch_Compose_Send: String public let Preview_CopyAddress: String @@ -2826,8 +3019,8 @@ public final class PresentationStrings { public let Channel_EditAdmin_PermissionChangeInfo: String public let Notifications_ResetAllNotificationsHelp: String public let DialogList_EncryptionRejected: String - public let AccessDenied_CameraRestricted: String public let Target_InviteToGroupErrorAlreadyInvited: String + public let AccessDenied_CameraRestricted: String public let Watch_Message_ForwardedFrom: String public let Channel_AboutItem: String public let PhotoEditor_CurvesGreen: String @@ -2851,6 +3044,11 @@ public final class PresentationStrings { public func Login_ResetAccountProtected_Text(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Login_ResetAccountProtected_Text, self._Login_ResetAccountProtected_Text_r, [_0]) } + private let _Channel_AdminLog_EmptyFilterQueryText: String + private let _Channel_AdminLog_EmptyFilterQueryText_r: [(Int, NSRange)] + public func Channel_AdminLog_EmptyFilterQueryText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_EmptyFilterQueryText, self._Channel_AdminLog_EmptyFilterQueryText_r, [_0]) + } public let Camera_TapAndHoldForVideo: String public let Bot_DescriptionTitle: String public let FeaturedStickerPacks_Title: String @@ -2927,13 +3125,14 @@ public final class PresentationStrings { public let Conversation_BroadcastTitle: String public let Username_Help: String public let StickerSettings_ContextHide: String - public let Weekday_Sunday: String public let Preview_LoadingImage: String + public let Weekday_Sunday: String private let _Conversation_DownloadProgressKilobytes: String private let _Conversation_DownloadProgressKilobytes_r: [(Int, NSRange)] public func Conversation_DownloadProgressKilobytes(_ _1: Int, _ _2: Int) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_DownloadProgressKilobytes, self._Conversation_DownloadProgressKilobytes_r, ["\(_1)", "\(_2)"]) } + public let Contacts_ShareTelegram: String public let Settings_ChatBackground: String private let _MessageTimer_Seconds_zero: String private let _MessageTimer_Seconds_one: String @@ -3001,28 +3200,6 @@ public final class PresentationStrings { return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") } } - private let _Notification_GameScoreSimple_zero: String - private let _Notification_GameScoreSimple_one: String - private let _Notification_GameScoreSimple_two: String - private let _Notification_GameScoreSimple_few: String - private let _Notification_GameScoreSimple_many: String - private let _Notification_GameScoreSimple_other: String - public func Notification_GameScoreSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSimple_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSimple_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSimple_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSimple_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSimple_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSimple_other, "\(value)") - } - } private let _Notification_GameScoreExtended_zero: String private let _Notification_GameScoreExtended_one: String private let _Notification_GameScoreExtended_two: String @@ -3045,6 +3222,28 @@ public final class PresentationStrings { return String(format: self._Notification_GameScoreExtended_other, "\(value)") } } + private let _Notification_GameScoreSimple_zero: String + private let _Notification_GameScoreSimple_one: String + private let _Notification_GameScoreSimple_two: String + private let _Notification_GameScoreSimple_few: String + private let _Notification_GameScoreSimple_many: String + private let _Notification_GameScoreSimple_other: String + public func Notification_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSimple_other, "\(value)") + } + } private let _PasscodeSettings_FailedAttempts_zero: String private let _PasscodeSettings_FailedAttempts_one: String private let _PasscodeSettings_FailedAttempts_two: String @@ -3243,28 +3442,6 @@ public final class PresentationStrings { return String(format: self._Call_ShortSeconds_other, "\(value)") } } - private let _Time_PreciseDate_zero: String - private let _Time_PreciseDate_one: String - private let _Time_PreciseDate_two: String - private let _Time_PreciseDate_few: String - private let _Time_PreciseDate_many: String - private let _Time_PreciseDate_other: String - public func Time_PreciseDate(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Time_PreciseDate_zero, "\(value)") - case .one: - return String(format: self._Time_PreciseDate_one, "\(value)") - case .two: - return String(format: self._Time_PreciseDate_two, "\(value)") - case .few: - return String(format: self._Time_PreciseDate_few, "\(value)") - case .many: - return String(format: self._Time_PreciseDate_many, "\(value)") - case .other: - return String(format: self._Time_PreciseDate_other, "\(value)") - } - } private let _SharedMedia_File_zero: String private let _SharedMedia_File_one: String private let _SharedMedia_File_two: String @@ -3529,28 +3706,6 @@ public final class PresentationStrings { return String(format: self._StickerPack_StickerCount_other, "\(value)") } } - private let _SharedMedia_Video_zero: String - private let _SharedMedia_Video_one: String - private let _SharedMedia_Video_two: String - private let _SharedMedia_Video_few: String - private let _SharedMedia_Video_many: String - private let _SharedMedia_Video_other: String - public func SharedMedia_Video(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Video_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Video_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Video_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Video_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Video_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Video_other, "\(value)") - } - } private let _ForwardedAuthorsOthers_zero: String private let _ForwardedAuthorsOthers_one: String private let _ForwardedAuthorsOthers_two: String @@ -3573,6 +3728,28 @@ public final class PresentationStrings { return String(format: self._ForwardedAuthorsOthers_other, "\(value)") } } + private let _SharedMedia_Video_zero: String + private let _SharedMedia_Video_one: String + private let _SharedMedia_Video_two: String + private let _SharedMedia_Video_few: String + private let _SharedMedia_Video_many: String + private let _SharedMedia_Video_other: String + public func SharedMedia_Video(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Video_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Video_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Video_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Video_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Video_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Video_other, "\(value)") + } + } private let _MuteFor_Minutes_zero: String private let _MuteFor_Minutes_one: String private let _MuteFor_Minutes_two: String @@ -3683,28 +3860,6 @@ public final class PresentationStrings { return String(format: self._Channel_NotificationComments_other, "\(value)") } } - private let _UserCount_zero: String - private let _UserCount_one: String - private let _UserCount_two: String - private let _UserCount_few: String - private let _UserCount_many: String - private let _UserCount_other: String - public func UserCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._UserCount_zero, "\(value)") - case .one: - return String(format: self._UserCount_one, "\(value)") - case .two: - return String(format: self._UserCount_two, "\(value)") - case .few: - return String(format: self._UserCount_few, "\(value)") - case .many: - return String(format: self._UserCount_many, "\(value)") - case .other: - return String(format: self._UserCount_other, "\(value)") - } - } private let _ForwardedGifs_zero: String private let _ForwardedGifs_one: String private let _ForwardedGifs_two: String @@ -3727,6 +3882,28 @@ public final class PresentationStrings { return String(format: self._ForwardedGifs_other, "\(value)") } } + private let _UserCount_zero: String + private let _UserCount_one: String + private let _UserCount_two: String + private let _UserCount_few: String + private let _UserCount_many: String + private let _UserCount_other: String + public func UserCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._UserCount_zero, "\(value)") + case .one: + return String(format: self._UserCount_one, "\(value)") + case .two: + return String(format: self._UserCount_two, "\(value)") + case .few: + return String(format: self._UserCount_few, "\(value)") + case .many: + return String(format: self._UserCount_many, "\(value)") + case .other: + return String(format: self._UserCount_other, "\(value)") + } + } private let _MessageTimer_ShortHours_zero: String private let _MessageTimer_ShortHours_one: String private let _MessageTimer_ShortHours_two: String @@ -3815,6 +3992,28 @@ public final class PresentationStrings { return String(format: self._AttachmentMenu_SendPhoto_other, "\(value)") } } + private let _LastSeen_MinutesAgo_zero: String + private let _LastSeen_MinutesAgo_one: String + private let _LastSeen_MinutesAgo_two: String + private let _LastSeen_MinutesAgo_few: String + private let _LastSeen_MinutesAgo_many: String + private let _LastSeen_MinutesAgo_other: String + public func LastSeen_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_MinutesAgo_other, "\(value)") + } + } private let _Conversation_StatusRecipients_zero: String private let _Conversation_StatusRecipients_one: String private let _Conversation_StatusRecipients_two: String @@ -3837,28 +4036,6 @@ public final class PresentationStrings { return String(format: self._Conversation_StatusRecipients_other, "\(value)") } } - private let _Channel_Management_LabelRights_zero: String - private let _Channel_Management_LabelRights_one: String - private let _Channel_Management_LabelRights_two: String - private let _Channel_Management_LabelRights_few: String - private let _Channel_Management_LabelRights_many: String - private let _Channel_Management_LabelRights_other: String - public func Channel_Management_LabelRights(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Channel_Management_LabelRights_zero, "\(value)") - case .one: - return String(format: self._Channel_Management_LabelRights_one, "\(value)") - case .two: - return String(format: self._Channel_Management_LabelRights_two, "\(value)") - case .few: - return String(format: self._Channel_Management_LabelRights_few, "\(value)") - case .many: - return String(format: self._Channel_Management_LabelRights_many, "\(value)") - case .other: - return String(format: self._Channel_Management_LabelRights_other, "\(value)") - } - } private let _ServiceMessage_GameScoreSelfSimple_zero: String private let _ServiceMessage_GameScoreSelfSimple_one: String private let _ServiceMessage_GameScoreSelfSimple_two: String @@ -3947,26 +4124,26 @@ public final class PresentationStrings { return String(format: self._StickerPack_AddMaskCount_other, "\(value)") } } - private let _LastSeen_MinutesAgo_zero: String - private let _LastSeen_MinutesAgo_one: String - private let _LastSeen_MinutesAgo_two: String - private let _LastSeen_MinutesAgo_few: String - private let _LastSeen_MinutesAgo_many: String - private let _LastSeen_MinutesAgo_other: String - public func LastSeen_MinutesAgo(_ value: Int32) -> String { + private let _Channel_Management_LabelRights_zero: String + private let _Channel_Management_LabelRights_one: String + private let _Channel_Management_LabelRights_two: String + private let _Channel_Management_LabelRights_few: String + private let _Channel_Management_LabelRights_many: String + private let _Channel_Management_LabelRights_other: String + public func Channel_Management_LabelRights(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._LastSeen_MinutesAgo_zero, "\(value)") + return String(format: self._Channel_Management_LabelRights_zero, "\(value)") case .one: - return String(format: self._LastSeen_MinutesAgo_one, "\(value)") + return String(format: self._Channel_Management_LabelRights_one, "\(value)") case .two: - return String(format: self._LastSeen_MinutesAgo_two, "\(value)") + return String(format: self._Channel_Management_LabelRights_two, "\(value)") case .few: - return String(format: self._LastSeen_MinutesAgo_few, "\(value)") + return String(format: self._Channel_Management_LabelRights_few, "\(value)") case .many: - return String(format: self._LastSeen_MinutesAgo_many, "\(value)") + return String(format: self._Channel_Management_LabelRights_many, "\(value)") case .other: - return String(format: self._LastSeen_MinutesAgo_other, "\(value)") + return String(format: self._Channel_Management_LabelRights_other, "\(value)") } } private let _LastSeen_HoursAgo_zero: String @@ -4189,26 +4366,26 @@ public final class PresentationStrings { return String(format: self._SharedMedia_DeleteItemsConfirmation_other, "\(value)") } } - private let _Watch_LastSeen_MinutesAgo_zero: String - private let _Watch_LastSeen_MinutesAgo_one: String - private let _Watch_LastSeen_MinutesAgo_two: String - private let _Watch_LastSeen_MinutesAgo_few: String - private let _Watch_LastSeen_MinutesAgo_many: String - private let _Watch_LastSeen_MinutesAgo_other: String - public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + private let _ForwardedVideos_zero: String + private let _ForwardedVideos_one: String + private let _ForwardedVideos_two: String + private let _ForwardedVideos_few: String + private let _ForwardedVideos_many: String + private let _ForwardedVideos_other: String + public func ForwardedVideos(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") + return String(format: self._ForwardedVideos_zero, "\(value)") case .one: - return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") + return String(format: self._ForwardedVideos_one, "\(value)") case .two: - return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") + return String(format: self._ForwardedVideos_two, "\(value)") case .few: - return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") + return String(format: self._ForwardedVideos_few, "\(value)") case .many: - return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") + return String(format: self._ForwardedVideos_many, "\(value)") case .other: - return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") + return String(format: self._ForwardedVideos_other, "\(value)") } } private let _ForwardedMessages_zero: String @@ -4255,26 +4432,26 @@ public final class PresentationStrings { return String(format: self._SharedMedia_ItemsSelected_other, "\(value)") } } - private let _MessageTimer_Hours_zero: String - private let _MessageTimer_Hours_one: String - private let _MessageTimer_Hours_two: String - private let _MessageTimer_Hours_few: String - private let _MessageTimer_Hours_many: String - private let _MessageTimer_Hours_other: String - public func MessageTimer_Hours(_ value: Int32) -> String { + private let _Watch_LastSeen_MinutesAgo_zero: String + private let _Watch_LastSeen_MinutesAgo_one: String + private let _Watch_LastSeen_MinutesAgo_two: String + private let _Watch_LastSeen_MinutesAgo_few: String + private let _Watch_LastSeen_MinutesAgo_many: String + private let _Watch_LastSeen_MinutesAgo_other: String + public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._MessageTimer_Hours_zero, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") case .one: - return String(format: self._MessageTimer_Hours_one, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") case .two: - return String(format: self._MessageTimer_Hours_two, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") case .few: - return String(format: self._MessageTimer_Hours_few, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") case .many: - return String(format: self._MessageTimer_Hours_many, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") case .other: - return String(format: self._MessageTimer_Hours_other, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") } } private let _MessageTimer_Years_zero: String @@ -4321,26 +4498,26 @@ public final class PresentationStrings { return String(format: self._Map_ETAMinutes_other, "\(value)") } } - private let _ForwardedVideos_zero: String - private let _ForwardedVideos_one: String - private let _ForwardedVideos_two: String - private let _ForwardedVideos_few: String - private let _ForwardedVideos_many: String - private let _ForwardedVideos_other: String - public func ForwardedVideos(_ value: Int32) -> String { + private let _MessageTimer_Hours_zero: String + private let _MessageTimer_Hours_one: String + private let _MessageTimer_Hours_two: String + private let _MessageTimer_Hours_few: String + private let _MessageTimer_Hours_many: String + private let _MessageTimer_Hours_other: String + public func MessageTimer_Hours(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._ForwardedVideos_zero, "\(value)") + return String(format: self._MessageTimer_Hours_zero, "\(value)") case .one: - return String(format: self._ForwardedVideos_one, "\(value)") + return String(format: self._MessageTimer_Hours_one, "\(value)") case .two: - return String(format: self._ForwardedVideos_two, "\(value)") + return String(format: self._MessageTimer_Hours_two, "\(value)") case .few: - return String(format: self._ForwardedVideos_few, "\(value)") + return String(format: self._MessageTimer_Hours_few, "\(value)") case .many: - return String(format: self._ForwardedVideos_many, "\(value)") + return String(format: self._MessageTimer_Hours_many, "\(value)") case .other: - return String(format: self._ForwardedVideos_other, "\(value)") + return String(format: self._MessageTimer_Hours_other, "\(value)") } } private let _Notification_GameScoreSelfSimple_zero: String @@ -4475,28 +4652,6 @@ public final class PresentationStrings { return String(format: self._AttachmentMenu_SendItem_other, "\(value)") } } - private let _Time_MonthOfYear_zero: String - private let _Time_MonthOfYear_one: String - private let _Time_MonthOfYear_two: String - private let _Time_MonthOfYear_few: String - private let _Time_MonthOfYear_many: String - private let _Time_MonthOfYear_other: String - public func Time_MonthOfYear(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Time_MonthOfYear_zero, "\(value)") - case .one: - return String(format: self._Time_MonthOfYear_one, "\(value)") - case .two: - return String(format: self._Time_MonthOfYear_two, "\(value)") - case .few: - return String(format: self._Time_MonthOfYear_few, "\(value)") - case .many: - return String(format: self._Time_MonthOfYear_many, "\(value)") - case .other: - return String(format: self._Time_MonthOfYear_other, "\(value)") - } - } private let _Watch_UserInfo_Mute_zero: String private let _Watch_UserInfo_Mute_one: String private let _Watch_UserInfo_Mute_two: String @@ -4607,28 +4762,6 @@ public final class PresentationStrings { return String(format: self._ForwardedLocations_other, "\(value)") } } - private let _MessageTimer_ShortWeeks_zero: String - private let _MessageTimer_ShortWeeks_one: String - private let _MessageTimer_ShortWeeks_two: String - private let _MessageTimer_ShortWeeks_few: String - private let _MessageTimer_ShortWeeks_many: String - private let _MessageTimer_ShortWeeks_other: String - public func MessageTimer_ShortWeeks(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") - } - } private let _MessageTimer_Minutes_zero: String private let _MessageTimer_Minutes_one: String private let _MessageTimer_Minutes_two: String @@ -4651,6 +4784,28 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Minutes_other, "\(value)") } } + private let _MessageTimer_ShortWeeks_zero: String + private let _MessageTimer_ShortWeeks_one: String + private let _MessageTimer_ShortWeeks_two: String + private let _MessageTimer_ShortWeeks_few: String + private let _MessageTimer_ShortWeeks_many: String + private let _MessageTimer_ShortWeeks_other: String + public func MessageTimer_ShortWeeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") + } + } private let _MessageTimer_Months_zero: String private let _MessageTimer_Months_one: String private let _MessageTimer_Months_two: String @@ -4741,9 +4896,9 @@ public final class PresentationStrings { self._Group_Username_LinkHint_r = extractArgumentRanges(self._Group_Username_LinkHint) self.Activity_UploadingPhoto = getValue(dict, "Activity.UploadingPhoto") self.PrivacySettings_PrivacyTitle = getValue(dict, "PrivacySettings.PrivacyTitle") + self.Settings_LogoutError = getValue(dict, "Settings.LogoutError") self._DialogList_PinLimitError = getValue(dict, "DialogList.PinLimitError") self._DialogList_PinLimitError_r = extractArgumentRanges(self._DialogList_PinLimitError) - self.Settings_LogoutError = getValue(dict, "Settings.LogoutError") self.Cache_ClearCache = getValue(dict, "Cache.ClearCache") self.Common_Close = getValue(dict, "Common.Close") self.ChangePhoneNumberCode_Called = getValue(dict, "ChangePhoneNumberCode.Called") @@ -4770,19 +4925,18 @@ public final class PresentationStrings { self.Group_MessagePhotoUpdated = getValue(dict, "Group.MessagePhotoUpdated") self.Message_PinnedInvoice = getValue(dict, "Message.PinnedInvoice") self.Login_InfoAvatarAdd = getValue(dict, "Login.InfoAvatarAdd") + self.Conversation_RestrictedMedia = getValue(dict, "Conversation.RestrictedMedia") self.WebSearch_RecentSectionTitle = getValue(dict, "WebSearch.RecentSectionTitle") self._CHAT_MESSAGE_TEXT = getValue(dict, "CHAT_MESSAGE_TEXT") self._CHAT_MESSAGE_TEXT_r = extractArgumentRanges(self._CHAT_MESSAGE_TEXT) self.Message_Sticker = getValue(dict, "Message.Sticker") self.Channel_Management_Remove = getValue(dict, "Channel.Management.Remove") - self.Paint_Regular = getValue(dict, "Paint.Regular") self.Channel_Username_Help = getValue(dict, "Channel.Username.Help") + self.Paint_Regular = getValue(dict, "Paint.Regular") self._Profile_CreateEncryptedChatOutdatedError = getValue(dict, "Profile.CreateEncryptedChatOutdatedError") self._Profile_CreateEncryptedChatOutdatedError_r = extractArgumentRanges(self._Profile_CreateEncryptedChatOutdatedError) self.Login_InactiveHelp = getValue(dict, "Login.InactiveHelp") self.ChatSettings_Security = getValue(dict, "ChatSettings.Security") - self._Time_PreciseDate_9 = getValue(dict, "Time.PreciseDate_9") - self._Time_PreciseDate_9_r = extractArgumentRanges(self._Time_PreciseDate_9) self._PINNED_STICKER = getValue(dict, "PINNED_STICKER") self._PINNED_STICKER_r = extractArgumentRanges(self._PINNED_STICKER) self.Conversation_ShareInlineBotLocationConfirmation = getValue(dict, "Conversation.ShareInlineBotLocationConfirmation") @@ -4800,6 +4954,7 @@ public final class PresentationStrings { self.PrivacyLastSeenSettings_NeverShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Placeholder") self.TwoStepAuth_SetupEmail = getValue(dict, "TwoStepAuth.SetupEmail") self.Login_ResetAccountProtected_Reset = getValue(dict, "Login.ResetAccountProtected.Reset") + self.SocksProxySetup_Hostname = getValue(dict, "SocksProxySetup.Hostname") self._MESSAGE_CONTACT = getValue(dict, "MESSAGE_CONTACT") self._MESSAGE_CONTACT_r = extractArgumentRanges(self._MESSAGE_CONTACT) self._Group_Management_ErrorNotMember = getValue(dict, "Group.Management.ErrorNotMember") @@ -4827,12 +4982,16 @@ public final class PresentationStrings { self.SharedMedia_EmptyLinksText = getValue(dict, "SharedMedia.EmptyLinksText") self.Channel_Members_Kick = getValue(dict, "Channel.Members.Kick") self.GoogleDrive_FolderIsEmpty = getValue(dict, "GoogleDrive.FolderIsEmpty") + self._Conversation_RestrictedTextTimed = getValue(dict, "Conversation.RestrictedTextTimed") + self._Conversation_RestrictedTextTimed_r = extractArgumentRanges(self._Conversation_RestrictedTextTimed) self.Calls_NoCallsPlaceholder = getValue(dict, "Calls.NoCallsPlaceholder") self.Message_PinnedDeletedMessage = getValue(dict, "Message.PinnedDeletedMessage") self.Conversation_PinMessageAlert_OnlyPin = getValue(dict, "Conversation.PinMessageAlert.OnlyPin") self.ReportPeer_ReasonOther_Send = getValue(dict, "ReportPeer.ReasonOther.Send") self.Conversation_InstantPagePreview = getValue(dict, "Conversation.InstantPagePreview") self.PasscodeSettings_SimplePasscodeHelp = getValue(dict, "PasscodeSettings.SimplePasscodeHelp") + self._Time_PreciseDate_m9 = getValue(dict, "Time.PreciseDate_m9") + self._Time_PreciseDate_m9_r = extractArgumentRanges(self._Time_PreciseDate_m9) self.Group_ErrorAddTooMuch = getValue(dict, "Group.ErrorAddTooMuch") self.GroupInfo_Title = getValue(dict, "GroupInfo.Title") self.State_Updating = getValue(dict, "State.Updating") @@ -4842,8 +5001,8 @@ public final class PresentationStrings { self._TwoStepAuth_PendingEmailHelp_r = extractArgumentRanges(self._TwoStepAuth_PendingEmailHelp) self.UserInfo_PhoneCall = getValue(dict, "UserInfo.PhoneCall") self.MusicPlayer_VoiceNote = getValue(dict, "MusicPlayer.VoiceNote") - self.Paint_Duplicate = getValue(dict, "Paint.Duplicate") self.Channel_Username_InvalidTaken = getValue(dict, "Channel.Username.InvalidTaken") + self.Paint_Duplicate = getValue(dict, "Paint.Duplicate") self._Profile_ShareContactGroupFormat = getValue(dict, "Profile.ShareContactGroupFormat") self._Profile_ShareContactGroupFormat_r = extractArgumentRanges(self._Profile_ShareContactGroupFormat) self.SecretChat_Title = getValue(dict, "SecretChat.Title") @@ -4851,19 +5010,19 @@ public final class PresentationStrings { self.Checkout_LiabilityAlertTitle = getValue(dict, "Checkout.LiabilityAlertTitle") self.GroupInfo_GroupNamePlaceholder = getValue(dict, "GroupInfo.GroupNamePlaceholder") self.Conversation_InfoBroadcastList = getValue(dict, "Conversation.InfoBroadcastList") + self._Time_PreciseDate_m11 = getValue(dict, "Time.PreciseDate_m11") + self._Time_PreciseDate_m11_r = extractArgumentRanges(self._Time_PreciseDate_m11) self._Notification_JoinedGroupByLink = getValue(dict, "Notification.JoinedGroupByLink") self._Notification_JoinedGroupByLink_r = extractArgumentRanges(self._Notification_JoinedGroupByLink) - self._Time_PreciseDate_8 = getValue(dict, "Time.PreciseDate_8") - self._Time_PreciseDate_8_r = extractArgumentRanges(self._Time_PreciseDate_8) - self.Login_HaveNotReceivedCodeInternal = getValue(dict, "Login.HaveNotReceivedCodeInternal") self.LoginPassword_Title = getValue(dict, "LoginPassword.Title") + self.Login_HaveNotReceivedCodeInternal = getValue(dict, "Login.HaveNotReceivedCodeInternal") self.Conversation_PlayVideo = getValue(dict, "Conversation.PlayVideo") self.PasscodeSettings_SimplePasscode = getValue(dict, "PasscodeSettings.SimplePasscode") self.Conversation_MicrophoneAccessDisabled = getValue(dict, "Conversation.MicrophoneAccessDisabled") self.NewContact_Title = getValue(dict, "NewContact.Title") self.Username_CheckingUsername = getValue(dict, "Username.CheckingUsername") - self.Login_ResetAccountProtected_TimerTitle = getValue(dict, "Login.ResetAccountProtected.TimerTitle") self.UserInfo_InviteBotToGroup = getValue(dict, "UserInfo.InviteBotToGroup") + self.Login_ResetAccountProtected_TimerTitle = getValue(dict, "Login.ResetAccountProtected.TimerTitle") self.Checkout_Email = getValue(dict, "Checkout.Email") self.CheckoutInfo_SaveInfo = getValue(dict, "CheckoutInfo.SaveInfo") self._ChangePhoneNumberCode_CallTimer = getValue(dict, "ChangePhoneNumberCode.CallTimer") @@ -4885,6 +5044,7 @@ public final class PresentationStrings { self.Conversation_SecretLinkPreviewAlert = getValue(dict, "Conversation.SecretLinkPreviewAlert") self.Channel_AdminLog_BanEmbedLinks = getValue(dict, "Channel.AdminLog.BanEmbedLinks") self.Cache_Videos = getValue(dict, "Cache.Videos") + self.Call_ReportSkip = getValue(dict, "Call.ReportSkip") self.NetworkUsageSettings_MediaImageDataSection = getValue(dict, "NetworkUsageSettings.MediaImageDataSection") self.TwoStepAuth_GenericHelp = getValue(dict, "TwoStepAuth.GenericHelp") self._DialogList_SingleRecordingAudioSuffix = getValue(dict, "DialogList.SingleRecordingAudioSuffix") @@ -4900,12 +5060,15 @@ public final class PresentationStrings { self._Profile_ShareBotPersonFormat = getValue(dict, "Profile.ShareBotPersonFormat") self._Profile_ShareBotPersonFormat_r = extractArgumentRanges(self._Profile_ShareBotPersonFormat) self.SearchImages_SearchImages = getValue(dict, "SearchImages.SearchImages") + self.SocksProxySetup_Title = getValue(dict, "SocksProxySetup.Title") self.SharedMedia_EmptyMusicText = getValue(dict, "SharedMedia.EmptyMusicText") self.Cache_ByPeerHeader = getValue(dict, "Cache.ByPeerHeader") self.Bot_GroupStatusReadsHistory = getValue(dict, "Bot.GroupStatusReadsHistory") self.TwoStepAuth_ResetAccountConfirmation = getValue(dict, "TwoStepAuth.ResetAccountConfirmation") self.CallSettings_Always = getValue(dict, "CallSettings.Always") self.SearchImages_DownloadCancelled = getValue(dict, "SearchImages.DownloadCancelled") + self.Channel_BanUser_Unban = getValue(dict, "Channel.BanUser.Unban") + self.Message_ImageExpired = getValue(dict, "Message.ImageExpired") self.Settings_LogoutConfirmationTitle = getValue(dict, "Settings.LogoutConfirmationTitle") self.UserInfo_FirstNamePlaceholder = getValue(dict, "UserInfo.FirstNamePlaceholder") self.ChatSettings_AutoPlayAudio = getValue(dict, "ChatSettings.AutoPlayAudio") @@ -4942,19 +5105,20 @@ public final class PresentationStrings { self._Notification_PinnedRoundMessage = getValue(dict, "Notification.PinnedRoundMessage") self._Notification_PinnedRoundMessage_r = extractArgumentRanges(self._Notification_PinnedRoundMessage) self.Conversation_DeleteGroup = getValue(dict, "Conversation.DeleteGroup") - self._Time_PreciseDate_7 = getValue(dict, "Time.PreciseDate_7") - self._Time_PreciseDate_7_r = extractArgumentRanges(self._Time_PreciseDate_7) + self.Settings_SaveEditedPhotos = getValue(dict, "Settings.SaveEditedPhotos") self.Channel_Management_LabelCreator = getValue(dict, "Channel.Management.LabelCreator") self._Notification_PinnedStickerMessage = getValue(dict, "Notification.PinnedStickerMessage") self._Notification_PinnedStickerMessage_r = extractArgumentRanges(self._Notification_PinnedStickerMessage) - self.Settings_SaveEditedPhotos = getValue(dict, "Settings.SaveEditedPhotos") self.PhotoEditor_QualityTool = getValue(dict, "PhotoEditor.QualityTool") self.Login_NetworkError = getValue(dict, "Login.NetworkError") self.TwoStepAuth_EnterPasswordForgot = getValue(dict, "TwoStepAuth.EnterPasswordForgot") self.Compose_ChannelMembers = getValue(dict, "Compose.ChannelMembers") + self._Channel_AdminLog_CaptionEdited = getValue(dict, "Channel.AdminLog.CaptionEdited") + self._Channel_AdminLog_CaptionEdited_r = extractArgumentRanges(self._Channel_AdminLog_CaptionEdited) self.Common_Yes = getValue(dict, "Common.Yes") self.KeyCommand_JumpToPreviousUnreadChat = getValue(dict, "KeyCommand.JumpToPreviousUnreadChat") self.CheckoutInfo_ReceiverInfoPhone = getValue(dict, "CheckoutInfo.ReceiverInfoPhone") + self.SocksProxySetup_TypeNone = getValue(dict, "SocksProxySetup.TypeNone") self.GroupInfo_AddParticipantTitle = getValue(dict, "GroupInfo.AddParticipantTitle") self._CHANNEL_MESSAGE_TEXT = getValue(dict, "CHANNEL_MESSAGE_TEXT") self._CHANNEL_MESSAGE_TEXT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_TEXT) @@ -5007,11 +5171,15 @@ public final class PresentationStrings { self.Channel_AdminLog_InfoPanelAlertText = getValue(dict, "Channel.AdminLog.InfoPanelAlertText") self.Watch_State_WaitingForNetwork = getValue(dict, "Watch.State.WaitingForNetwork") self.Cache_Photos = getValue(dict, "Cache.Photos") + self._Channel_AdminLog_MessageUnpinned = getValue(dict, "Channel.AdminLog.MessageUnpinned") + self._Channel_AdminLog_MessageUnpinned_r = extractArgumentRanges(self._Channel_AdminLog_MessageUnpinned) self.Message_PinnedStickerMessage = getValue(dict, "Message.PinnedStickerMessage") self.PhotoEditor_QualityMedium = getValue(dict, "PhotoEditor.QualityMedium") self.Privacy_PaymentsClearInfo = getValue(dict, "Privacy.PaymentsClearInfo") self.PhotoEditor_CurvesRed = getValue(dict, "PhotoEditor.CurvesRed") self.Privacy_PaymentsTitle = getValue(dict, "Privacy.PaymentsTitle") + self._Time_PreciseDate_m8 = getValue(dict, "Time.PreciseDate_m8") + self._Time_PreciseDate_m8_r = extractArgumentRanges(self._Time_PreciseDate_m8) self.Login_PhoneNumberHelp = getValue(dict, "Login.PhoneNumberHelp") self.User_DeletedAccount = getValue(dict, "User.DeletedAccount") self.Call_StatusFailed = getValue(dict, "Call.StatusFailed") @@ -5027,24 +5195,25 @@ public final class PresentationStrings { self.Compose_NewGroup = getValue(dict, "Compose.NewGroup") self.TwoStepAuth_EmailPlaceholder = getValue(dict, "TwoStepAuth.EmailPlaceholder") self.PhotoEditor_ExposureTool = getValue(dict, "PhotoEditor.ExposureTool") + self.Conversation_ViewChannel = getValue(dict, "Conversation.ViewChannel") self.ChatAdmins_AdminLabel = getValue(dict, "ChatAdmins.AdminLabel") self.Contacts_FailedToSendInvitesMessage = getValue(dict, "Contacts.FailedToSendInvitesMessage") self.Login_Code = getValue(dict, "Login.Code") self.Channel_Username_InvalidCharacters = getValue(dict, "Channel.Username.InvalidCharacters") - self.Calls_CallTabTitle = getValue(dict, "Calls.CallTabTitle") self.FeatureDisabled_Oops = getValue(dict, "FeatureDisabled.Oops") self.Login_InviteButton = getValue(dict, "Login.InviteButton") self.ShareMenu_Send = getValue(dict, "ShareMenu.Send") self.Conversation_InfoGroup = getValue(dict, "Conversation.InfoGroup") self.WatchRemote_AlertTitle = getValue(dict, "WatchRemote.AlertTitle") self.Preview_ProfilePhotoTitle = getValue(dict, "Preview.ProfilePhotoTitle") + self.Calls_CallTabTitle = getValue(dict, "Calls.CallTabTitle") + self.Channel_Members_AddBannedErrorAdmin = getValue(dict, "Channel.Members.AddBannedErrorAdmin") self.Checkout_Phone = getValue(dict, "Checkout.Phone") self.Channel_SignMessages_Help = getValue(dict, "Channel.SignMessages.Help") self.Calls_SubmitRating = getValue(dict, "Calls.SubmitRating") self.Camera_FlashOn = getValue(dict, "Camera.FlashOn") self.Watch_MessageView_Forward = getValue(dict, "Watch.MessageView.Forward") - self._Time_PreciseDate_6 = getValue(dict, "Time.PreciseDate_6") - self._Time_PreciseDate_6_r = extractArgumentRanges(self._Time_PreciseDate_6) + self.GroupInfo_ActionPromote = getValue(dict, "GroupInfo.ActionPromote") self.DialogList_You = getValue(dict, "DialogList.You") self.Weekday_Monday = getValue(dict, "Weekday.Monday") self.Watch_Suggestion_Yes = getValue(dict, "Watch.Suggestion.Yes") @@ -5086,6 +5255,8 @@ public final class PresentationStrings { self.Your_cards_security_code_is_invalid = getValue(dict, "Your_cards_security_code_is_invalid") self.Tour_StartButton = getValue(dict, "Tour.StartButton") self.CheckoutInfo_Title = getValue(dict, "CheckoutInfo.Title") + self._Channel_AdminLog_MessageRestrictedNameUsername = getValue(dict, "Channel.AdminLog.MessageRestrictedNameUsername") + self._Channel_AdminLog_MessageRestrictedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNameUsername) self.ChangePhoneNumberCode_Help = getValue(dict, "ChangePhoneNumberCode.Help") self.Web_Error = getValue(dict, "Web.Error") self.ShareFileTip_Title = getValue(dict, "ShareFileTip.Title") @@ -5114,14 +5285,14 @@ public final class PresentationStrings { self.TwoStepAuth_SetPasswordHelp = getValue(dict, "TwoStepAuth.SetPasswordHelp") self.Channel_AdminLogFilter_EventsTitle = getValue(dict, "Channel.AdminLogFilter.EventsTitle") self.Username_LinkCopied = getValue(dict, "Username.LinkCopied") + self._Time_MonthOfYear_m9 = getValue(dict, "Time.MonthOfYear_m9") + self._Time_MonthOfYear_m9_r = extractArgumentRanges(self._Time_MonthOfYear_m9) self.DialogList_Conversations = getValue(dict, "DialogList.Conversations") self.Channel_EditAdmin_PermissionAddAdmins = getValue(dict, "Channel.EditAdmin.PermissionAddAdmins") self.Conversation_SendMessage = getValue(dict, "Conversation.SendMessage") self.Notification_CallIncoming = getValue(dict, "Notification.CallIncoming") self._MESSAGE_FWDS = getValue(dict, "MESSAGE_FWDS") self._MESSAGE_FWDS_r = extractArgumentRanges(self._MESSAGE_FWDS) - self._Time_PreciseDate_5 = getValue(dict, "Time.PreciseDate_5") - self._Time_PreciseDate_5_r = extractArgumentRanges(self._Time_PreciseDate_5) self.Conversation_InputTextCommentPlaceholder = getValue(dict, "Conversation.InputTextCommentPlaceholder") self.Map_OpenInYandexMaps = getValue(dict, "Map.OpenInYandexMaps") self.Month_ShortNovember = getValue(dict, "Month.ShortNovember") @@ -5159,10 +5330,10 @@ public final class PresentationStrings { self.DialogList_DeleteBotConfirmation = getValue(dict, "DialogList.DeleteBotConfirmation") self.Common_TakePhotoOrVideo = getValue(dict, "Common.TakePhotoOrVideo") self.Notification_MessageLifetime2s = getValue(dict, "Notification.MessageLifetime2s") - self.Checkout_ErrorGeneric = getValue(dict, "Checkout.ErrorGeneric") self.Conversation_FileGoogleDrive = getValue(dict, "Conversation.FileGoogleDrive") self._MediaPicker_Processing = getValue(dict, "MediaPicker.Processing") self._MediaPicker_Processing_r = extractArgumentRanges(self._MediaPicker_Processing) + self.Checkout_ErrorGeneric = getValue(dict, "Checkout.ErrorGeneric") self.Channel_AdminLog_CanBanUsers = getValue(dict, "Channel.AdminLog.CanBanUsers") self.Cache_Indexing = getValue(dict, "Cache.Indexing") self._ENCRYPTION_REQUEST = getValue(dict, "ENCRYPTION_REQUEST") @@ -5174,8 +5345,11 @@ public final class PresentationStrings { self.GroupInfo_InviteLink_LinkSection = getValue(dict, "GroupInfo.InviteLink.LinkSection") self.Privacy_Calls_AlwaysAllow_Placeholder = getValue(dict, "Privacy.Calls.AlwaysAllow.Placeholder") self.CheckoutInfo_ShippingInfoPostcode = getValue(dict, "CheckoutInfo.ShippingInfoPostcode") + self._Time_PreciseDate_m7 = getValue(dict, "Time.PreciseDate_m7") + self._Time_PreciseDate_m7_r = extractArgumentRanges(self._Time_PreciseDate_m7) self.PasscodeSettings_EncryptDataHelp = getValue(dict, "PasscodeSettings.EncryptDataHelp") self.KeyCommand_FocusOnInputField = getValue(dict, "KeyCommand.FocusOnInputField") + self.Channel_Members_AddAdminErrorBlacklisted = getValue(dict, "Channel.Members.AddAdminErrorBlacklisted") self.Cache_KeepMedia = getValue(dict, "Cache.KeepMedia") self.WebPreview_GettingLinkInfo = getValue(dict, "WebPreview.GettingLinkInfo") self.Group_Setup_TypePublicHelp = getValue(dict, "Group.Setup.TypePublicHelp") @@ -5193,18 +5367,18 @@ public final class PresentationStrings { self.Watch_Suggestion_WhatsUp = getValue(dict, "Watch.Suggestion.WhatsUp") self.LoginPassword_PasswordPlaceholder = getValue(dict, "LoginPassword.PasswordPlaceholder") self.TwoStepAuth_EnterPasswordPassword = getValue(dict, "TwoStepAuth.EnterPasswordPassword") + self._Time_PreciseDate_m10 = getValue(dict, "Time.PreciseDate_m10") + self._Time_PreciseDate_m10_r = extractArgumentRanges(self._Time_PreciseDate_m10) self._CHANNEL_MESSAGE_CONTACT = getValue(dict, "CHANNEL_MESSAGE_CONTACT") self._CHANNEL_MESSAGE_CONTACT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_CONTACT) self.PrivacySettings_DeleteAccountHelp = getValue(dict, "PrivacySettings.DeleteAccountHelp") - self._Time_PreciseDate_4 = getValue(dict, "Time.PreciseDate_4") - self._Time_PreciseDate_4_r = extractArgumentRanges(self._Time_PreciseDate_4) self.Channel_Info_Banned = getValue(dict, "Channel.Info.Banned") self.Conversation_ShareBotContactConfirmationTitle = getValue(dict, "Conversation.ShareBotContactConfirmationTitle") self.ConversationProfile_UsersTooMuchError = getValue(dict, "ConversationProfile.UsersTooMuchError") self.ChatAdmins_AllMembersAreAdminsOffHelp = getValue(dict, "ChatAdmins.AllMembersAreAdminsOffHelp") self.Privacy_GroupsAndChannels_WhoCanAddMe = getValue(dict, "Privacy.GroupsAndChannels.WhoCanAddMe") - self.Settings_PhoneNumber = getValue(dict, "Settings.PhoneNumber") self.Login_CodeExpiredError = getValue(dict, "Login.CodeExpiredError") + self.Settings_PhoneNumber = getValue(dict, "Settings.PhoneNumber") self._DialogList_MultipleTypingSuffix = getValue(dict, "DialogList.MultipleTypingSuffix") self._DialogList_MultipleTypingSuffix_r = extractArgumentRanges(self._DialogList_MultipleTypingSuffix) self.ChannelMembers_Blacklist_EmptyText = getValue(dict, "ChannelMembers.Blacklist.EmptyText") @@ -5215,6 +5389,7 @@ public final class PresentationStrings { self._Notification_Kicked = getValue(dict, "Notification.Kicked") self._Notification_Kicked_r = extractArgumentRanges(self._Notification_Kicked) self.Conversation_Send = getValue(dict, "Conversation.Send") + self.Channel_AdminLog_MessageRestrictedForever = getValue(dict, "Channel.AdminLog.MessageRestrictedForever") self.ChannelInfo_DeleteChannelConfirmation = getValue(dict, "ChannelInfo.DeleteChannelConfirmation") self.Weekday_ShortSaturday = getValue(dict, "Weekday.ShortSaturday") self.Map_SendThisLocation = getValue(dict, "Map.SendThisLocation") @@ -5230,6 +5405,7 @@ public final class PresentationStrings { self.PhotoEditor_ContrastTool = getValue(dict, "PhotoEditor.ContrastTool") self.MediaPicker_MomentsDateYearFormat = getValue(dict, "MediaPicker.MomentsDateYearFormat") self.CheckoutInfo_ReceiverInfoNamePlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoNamePlaceholder") + self.Channel_AdminLog_MessagePreviousCaption = getValue(dict, "Channel.AdminLog.MessagePreviousCaption") self.Privacy_PaymentsClear_ShippingInfo = getValue(dict, "Privacy.PaymentsClear.ShippingInfo") self.TwoStepAuth_GenericError = getValue(dict, "TwoStepAuth.GenericError") self.Channel_Moderator_AccessLevelEditorHelp = getValue(dict, "Channel.Moderator.AccessLevelEditorHelp") @@ -5237,6 +5413,7 @@ public final class PresentationStrings { self.ConversationMedia_EmptyTitle = getValue(dict, "ConversationMedia.EmptyTitle") self.Date_DialogDateFormat = getValue(dict, "Date.DialogDateFormat") self.ReportPeer_ReasonSpam = getValue(dict, "ReportPeer.ReasonSpam") + self.Privacy_Calls_P2P = getValue(dict, "Privacy.Calls.P2P") self.Compose_TokenListPlaceholder = getValue(dict, "Compose.TokenListPlaceholder") self._PINNED_VIDEO = getValue(dict, "PINNED_VIDEO") self._PINNED_VIDEO_r = extractArgumentRanges(self._PINNED_VIDEO) @@ -5255,14 +5432,15 @@ public final class PresentationStrings { self.ChangePhoneNumberCode_RequestingACall = getValue(dict, "ChangePhoneNumberCode.RequestingACall") self.PrivacyLastSeenSettings_NeverShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Title") self.KeyCommand_JumpToNextChat = getValue(dict, "KeyCommand.JumpToNextChat") + self._Time_MonthOfYear_m8 = getValue(dict, "Time.MonthOfYear_m8") + self._Time_MonthOfYear_m8_r = extractArgumentRanges(self._Time_MonthOfYear_m8) self.Tour_Text1 = getValue(dict, "Tour.Text1") self.StickerPack_Remove = getValue(dict, "StickerPack.Remove") self.Conversation_HoldForVideo = getValue(dict, "Conversation.HoldForVideo") self.Checkout_NewCard_Title = getValue(dict, "Checkout.NewCard.Title") self.Channel_TitleInfo = getValue(dict, "Channel.TitleInfo") + self.State_ConnectingToProxy = getValue(dict, "State.ConnectingToProxy") self.Settings_About_Help = getValue(dict, "Settings.About.Help") - self._Time_PreciseDate_3 = getValue(dict, "Time.PreciseDate_3") - self._Time_PreciseDate_3_r = extractArgumentRanges(self._Time_PreciseDate_3) self.Watch_Conversation_Reply = getValue(dict, "Watch.Conversation.Reply") self.ShareMenu_CopyShareLink = getValue(dict, "ShareMenu.CopyShareLink") self.Channel_Setup_TypePrivateHelp = getValue(dict, "Channel.Setup.TypePrivateHelp") @@ -5284,9 +5462,10 @@ public final class PresentationStrings { self._Notification_PinnedTextMessage_r = extractArgumentRanges(self._Notification_PinnedTextMessage) self.GroupInfo_InvitationLinkDoesNotExist = getValue(dict, "GroupInfo.InvitationLinkDoesNotExist") self.ReportPeer_ReasonOther_Placeholder = getValue(dict, "ReportPeer.ReasonOther.Placeholder") - self.PasscodeSettings_AutoLock_Disabled = getValue(dict, "PasscodeSettings.AutoLock.Disabled") self.Wallpaper_Title = getValue(dict, "Wallpaper.Title") + self.PasscodeSettings_AutoLock_Disabled = getValue(dict, "PasscodeSettings.AutoLock.Disabled") self.Watch_Compose_CreateMessage = getValue(dict, "Watch.Compose.CreateMessage") + self.ChatSettings_ConnectionType_UseProxy = getValue(dict, "ChatSettings.ConnectionType.UseProxy") self.Message_Audio = getValue(dict, "Message.Audio") self.Notification_CreatedGroup = getValue(dict, "Notification.CreatedGroup") self.Conversation_SearchNoResults = getValue(dict, "Conversation.SearchNoResults") @@ -5307,15 +5486,15 @@ public final class PresentationStrings { self.Bot_Unblock = getValue(dict, "Bot.Unblock") self.SharedMedia_CategoryMedia = getValue(dict, "SharedMedia.CategoryMedia") self.Conversation_HoldForAudio = getValue(dict, "Conversation.HoldForAudio") - self.Conversation_ClousStorageInfo_Description1 = getValue(dict, "Conversation.ClousStorageInfo.Description1") self.Channel_Members_InviteLink = getValue(dict, "Channel.Members.InviteLink") - self.WebSearch_RecentClearConfirmation = getValue(dict, "WebSearch.RecentClearConfirmation") self.Core_ServiceUserStatus = getValue(dict, "Core.ServiceUserStatus") + self.WebSearch_RecentClearConfirmation = getValue(dict, "WebSearch.RecentClearConfirmation") + self.Conversation_ClousStorageInfo_Description1 = getValue(dict, "Conversation.ClousStorageInfo.Description1") self.Notification_ChannelMigratedFrom = getValue(dict, "Notification.ChannelMigratedFrom") self.Settings_Title = getValue(dict, "Settings.Title") self.Call_StatusBusy = getValue(dict, "Call.StatusBusy") - self.ArchivedPacksAlert_Title = getValue(dict, "ArchivedPacksAlert.Title") self.ConversationMedia_Title = getValue(dict, "ConversationMedia.Title") + self.ArchivedPacksAlert_Title = getValue(dict, "ArchivedPacksAlert.Title") self._Conversation_MessageViaUser = getValue(dict, "Conversation.MessageViaUser") self._Conversation_MessageViaUser_r = extractArgumentRanges(self._Conversation_MessageViaUser) self.Presence_invisible = getValue(dict, "Presence.invisible") @@ -5332,6 +5511,8 @@ public final class PresentationStrings { self._Conversation_BotInteractiveUrlAlert = getValue(dict, "Conversation.BotInteractiveUrlAlert") self._Conversation_BotInteractiveUrlAlert_r = extractArgumentRanges(self._Conversation_BotInteractiveUrlAlert) self.GroupInfo_SharedMedia = getValue(dict, "GroupInfo.SharedMedia") + self._Time_PreciseDate_m6 = getValue(dict, "Time.PreciseDate_m6") + self._Time_PreciseDate_m6_r = extractArgumentRanges(self._Time_PreciseDate_m6) self.Channel_Username_InvalidStartsWithNumber = getValue(dict, "Channel.Username.InvalidStartsWithNumber") self.KeyCommand_JumpToPreviousChat = getValue(dict, "KeyCommand.JumpToPreviousChat") self.Conversation_Call = getValue(dict, "Conversation.Call") @@ -5379,9 +5560,11 @@ public final class PresentationStrings { self.PasscodeSettings_AutoLock_IfAwayFor_5hours = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5hours") self.Notifications_Title = getValue(dict, "Notifications.Title") self.Conversation_PinnedMessage = getValue(dict, "Conversation.PinnedMessage") - self.Channel_AdminLog_MessagePreviousMessage = getValue(dict, "Channel.AdminLog.MessagePreviousMessage") + self._Time_MonthOfYear_m12 = getValue(dict, "Time.MonthOfYear_m12") + self._Time_MonthOfYear_m12_r = extractArgumentRanges(self._Time_MonthOfYear_m12) self.ConversationProfile_LeaveDeleteAndExit = getValue(dict, "ConversationProfile.LeaveDeleteAndExit") self.State_connecting = getValue(dict, "State.connecting") + self.Channel_AdminLog_MessagePreviousMessage = getValue(dict, "Channel.AdminLog.MessagePreviousMessage") self.WebPreview_LinkPreview = getValue(dict, "WebPreview.LinkPreview") self.Map_OpenInHereMaps = getValue(dict, "Map.OpenInHereMaps") self.CheckoutInfo_Pay = getValue(dict, "CheckoutInfo.Pay") @@ -5391,19 +5574,26 @@ public final class PresentationStrings { self.Map_OpenInGooglePlus = getValue(dict, "Map.OpenInGooglePlus") self._CHAT_MESSAGE_AUDIO = getValue(dict, "CHAT_MESSAGE_AUDIO") self._CHAT_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHAT_MESSAGE_AUDIO) - self.Login_SmsRequestState2 = getValue(dict, "Login.SmsRequestState2") self.Preview_SaveToCameraRoll = getValue(dict, "Preview.SaveToCameraRoll") + self.Login_SmsRequestState2 = getValue(dict, "Login.SmsRequestState2") self.PasscodeSettings_ChangePasscode = getValue(dict, "PasscodeSettings.ChangePasscode") self.TwoStepAuth_RecoveryCodeInvalid = getValue(dict, "TwoStepAuth.RecoveryCodeInvalid") self._Message_PaymentSent = getValue(dict, "Message.PaymentSent") self._Message_PaymentSent_r = extractArgumentRanges(self._Message_PaymentSent) self.Message_PinnedAudioMessage = getValue(dict, "Message.PinnedAudioMessage") + self.ChatSettings_ConnectionType_Title = getValue(dict, "ChatSettings.ConnectionType.Title") + self._Conversation_RestrictedMediaTimed = getValue(dict, "Conversation.RestrictedMediaTimed") + self._Conversation_RestrictedMediaTimed_r = extractArgumentRanges(self._Conversation_RestrictedMediaTimed) self.Login_InfoDeletePhoto = getValue(dict, "Login.InfoDeletePhoto") + self.Group_Members_AddMemberErrorNotAllowed = getValue(dict, "Group.Members.AddMemberErrorNotAllowed") self.Settings_SaveIncomingPhotosHelp = getValue(dict, "Settings.SaveIncomingPhotosHelp") self.TwoStepAuth_RecoveryCodeExpired = getValue(dict, "TwoStepAuth.RecoveryCodeExpired") self.TwoStepAuth_EmailTitle = getValue(dict, "TwoStepAuth.EmailTitle") self.Privacy_GroupsAndChannels_NeverAllow = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow") + self.Conversation_RestrictedStickers = getValue(dict, "Conversation.RestrictedStickers") self.Conversation_AddContact = getValue(dict, "Conversation.AddContact") + self._Time_MonthOfYear_m7 = getValue(dict, "Time.MonthOfYear_m7") + self._Time_MonthOfYear_m7_r = extractArgumentRanges(self._Time_MonthOfYear_m7) self.PhotoEditor_QualityLow = getValue(dict, "PhotoEditor.QualityLow") self.Paint_Outlined = getValue(dict, "Paint.Outlined") self.Checkout_PasswordEntry_Title = getValue(dict, "Checkout.PasswordEntry.Title") @@ -5411,8 +5601,8 @@ public final class PresentationStrings { self.PrivacySettings_LastSeenContacts = getValue(dict, "PrivacySettings.LastSeenContacts") self.CheckoutInfo_ShippingInfoAddress1 = getValue(dict, "CheckoutInfo.ShippingInfoAddress1") self.UserInfo_LastNamePlaceholder = getValue(dict, "UserInfo.LastNamePlaceholder") - self.Conversation_StatusKickedFromChannel = getValue(dict, "Conversation.StatusKickedFromChannel") self.GroupInfo_InviteLink_RevokeAlert_Text = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Text") + self.Conversation_StatusKickedFromChannel = getValue(dict, "Conversation.StatusKickedFromChannel") self._DialogList_SingleTypingSuffix = getValue(dict, "DialogList.SingleTypingSuffix") self._DialogList_SingleTypingSuffix_r = extractArgumentRanges(self._DialogList_SingleTypingSuffix) self.LastSeen_JustNow = getValue(dict, "LastSeen.JustNow") @@ -5425,11 +5615,10 @@ public final class PresentationStrings { self.Settings_About_Title = getValue(dict, "Settings.About.Title") self.PhoneNumberHelp_Help = getValue(dict, "PhoneNumberHelp.Help") self.Service_NetworkConfigurationUpdatedMessage = getValue(dict, "Service.NetworkConfigurationUpdatedMessage") - self._Time_MonthOfYear_9 = getValue(dict, "Time.MonthOfYear_9") - self._Time_MonthOfYear_9_r = extractArgumentRanges(self._Time_MonthOfYear_9) self.Channel_LinkItem = getValue(dict, "Channel.LinkItem") self.Camera_Retake = getValue(dict, "Camera.Retake") self.StickerPack_ShowStickers = getValue(dict, "StickerPack.ShowStickers") + self.Conversation_RestrictedText = getValue(dict, "Conversation.RestrictedText") self._CHAT_CREATED = getValue(dict, "CHAT_CREATED") self._CHAT_CREATED_r = extractArgumentRanges(self._CHAT_CREATED) self.LastSeen_WithinAMonth = getValue(dict, "LastSeen.WithinAMonth") @@ -5452,8 +5641,8 @@ public final class PresentationStrings { self.GroupInfo_DeleteAndExit = getValue(dict, "GroupInfo.DeleteAndExit") self.GroupInfo_InviteLink_CopyLink = getValue(dict, "GroupInfo.InviteLink.CopyLink") self.Weekday_Friday = getValue(dict, "Weekday.Friday") - self.Login_ResetAccountProtected_Title = getValue(dict, "Login.ResetAccountProtected.Title") self.Settings_SetProfilePhoto = getValue(dict, "Settings.SetProfilePhoto") + self.Login_ResetAccountProtected_Title = getValue(dict, "Login.ResetAccountProtected.Title") self.Compose_ChannelTokenListPlaceholder = getValue(dict, "Compose.ChannelTokenListPlaceholder") self.Channel_EditAdmin_PermissionPinMessages = getValue(dict, "Channel.EditAdmin.PermissionPinMessages") self.Your_card_has_expired = getValue(dict, "Your_card_has_expired") @@ -5466,11 +5655,11 @@ public final class PresentationStrings { self._Username_UsernameIsAvailable_r = extractArgumentRanges(self._Username_UsernameIsAvailable) self.KeyCommand_JumpToNextUnreadChat = getValue(dict, "KeyCommand.JumpToNextUnreadChat") self.Conversation_EncryptedDescriptionTitle = getValue(dict, "Conversation.EncryptedDescriptionTitle") - self.DialogList_Pin = getValue(dict, "DialogList.Pin") self._Notification_RemovedGroupPhoto = getValue(dict, "Notification.RemovedGroupPhoto") self._Notification_RemovedGroupPhoto_r = extractArgumentRanges(self._Notification_RemovedGroupPhoto) - self.Channel_ErrorAddTooMuch = getValue(dict, "Channel.ErrorAddTooMuch") self.GroupInfo_SharedMediaNone = getValue(dict, "GroupInfo.SharedMediaNone") + self.Channel_ErrorAddTooMuch = getValue(dict, "Channel.ErrorAddTooMuch") + self.DialogList_Pin = getValue(dict, "DialogList.Pin") self.ChatSettings_TextSizeUnits = getValue(dict, "ChatSettings.TextSizeUnits") self.ChatSettings_AutoPlayAnimations = getValue(dict, "ChatSettings.AutoPlayAnimations") self.Conversation_FileOpenIn = getValue(dict, "Conversation.FileOpenIn") @@ -5480,9 +5669,14 @@ public final class PresentationStrings { self.DialogList_RecentTitleGroups = getValue(dict, "DialogList.RecentTitleGroups") self.Privacy_GroupsAndChannels_CustomShareHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomShareHelp") self.KeyCommand_ChatInfo = getValue(dict, "KeyCommand.ChatInfo") + self.Channel_AdminLog_EmptyFilterTitle = getValue(dict, "Channel.AdminLog.EmptyFilterTitle") self.Notification_CreatedBroadcastList = getValue(dict, "Notification.CreatedBroadcastList") self.PhotoEditor_HighlightsTint = getValue(dict, "PhotoEditor.HighlightsTint") self.Watch_Compose_AddContact = getValue(dict, "Watch.Compose.AddContact") + self._Time_PreciseDate_m5 = getValue(dict, "Time.PreciseDate_m5") + self._Time_PreciseDate_m5_r = extractArgumentRanges(self._Time_PreciseDate_m5) + self._Channel_AdminLog_MessageKickedNameUsername = getValue(dict, "Channel.AdminLog.MessageKickedNameUsername") + self._Channel_AdminLog_MessageKickedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageKickedNameUsername) self.Coub_TapForSound = getValue(dict, "Coub.TapForSound") self.Compose_NewEncryptedChat = getValue(dict, "Compose.NewEncryptedChat") self.PhotoEditor_CropReset = getValue(dict, "PhotoEditor.CropReset") @@ -5496,8 +5690,8 @@ public final class PresentationStrings { self._Generic_OpenHiddenLinkAlert = getValue(dict, "Generic.OpenHiddenLinkAlert") self._Generic_OpenHiddenLinkAlert_r = extractArgumentRanges(self._Generic_OpenHiddenLinkAlert) self.Conversation_Contact = getValue(dict, "Conversation.Contact") - self.NetworkUsageSettings_GeneralDataSection = getValue(dict, "NetworkUsageSettings.GeneralDataSection") self.Service_ApplyLocalization = getValue(dict, "Service.ApplyLocalization") + self.NetworkUsageSettings_GeneralDataSection = getValue(dict, "NetworkUsageSettings.GeneralDataSection") self._StickerPack_RemovePrompt = getValue(dict, "StickerPack.RemovePrompt") self._StickerPack_RemovePrompt_r = extractArgumentRanges(self._StickerPack_RemovePrompt) self.Channel_NotificationCommentsDisabled = getValue(dict, "Channel.NotificationCommentsDisabled") @@ -5512,18 +5706,16 @@ public final class PresentationStrings { self.Conversation_ContextMenuDelete = getValue(dict, "Conversation.ContextMenuDelete") self.Tour_Text6 = getValue(dict, "Tour.Text6") self.PhotoEditor_WarmthTool = getValue(dict, "PhotoEditor.WarmthTool") - self._Time_MonthOfYear_8 = getValue(dict, "Time.MonthOfYear_8") - self._Time_MonthOfYear_8_r = extractArgumentRanges(self._Time_MonthOfYear_8) self.Common_TakePhoto = getValue(dict, "Common.TakePhoto") self.PhotoEditor_Current = getValue(dict, "PhotoEditor.Current") self.UserInfo_CreateNewContact = getValue(dict, "UserInfo.CreateNewContact") - self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") - self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") self.Watch_PhotoView_Title = getValue(dict, "Watch.PhotoView.Title") self._PrivacySettings_LastSeenContactsMinus = getValue(dict, "PrivacySettings.LastSeenContactsMinus") self._PrivacySettings_LastSeenContactsMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsMinus) self.Login_InfoUpdatePhoto = getValue(dict, "Login.InfoUpdatePhoto") + self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") self.ShareMenu_SelectChats = getValue(dict, "ShareMenu.SelectChats") + self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") self.Group_ErrorSendRestrictedMedia = getValue(dict, "Group.ErrorSendRestrictedMedia") self.Channel_EditAdmin_PermissinAddAdminOff = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOff") self.Cache_Files = getValue(dict, "Cache.Files") @@ -5559,6 +5751,7 @@ public final class PresentationStrings { self.Weekday_Yesterday = getValue(dict, "Weekday.Yesterday") self.Conversation_InputTextSilentBroadcastPlaceholder = getValue(dict, "Conversation.InputTextSilentBroadcastPlaceholder") self.Embed_PlayingInPIP = getValue(dict, "Embed.PlayingInPIP") + self.Localization_EnglishLanguageName = getValue(dict, "Localization.EnglishLanguageName") self.Call_StatusIncoming = getValue(dict, "Call.StatusIncoming") self.Conversation_Play = getValue(dict, "Conversation.Play") self.Settings_PrivacySettings = getValue(dict, "Settings.PrivacySettings") @@ -5573,10 +5766,14 @@ public final class PresentationStrings { self.Compose_NewChannel_AddMemberHelp = getValue(dict, "Compose.NewChannel.AddMemberHelp") self.GroupInfo_ChatAdmins = getValue(dict, "GroupInfo.ChatAdmins") self.PhotoEditor_CurvesAll = getValue(dict, "PhotoEditor.CurvesAll") + self._Notification_LeftChannel = getValue(dict, "Notification.LeftChannel") + self._Notification_LeftChannel_r = extractArgumentRanges(self._Notification_LeftChannel) self.Compose_Create = getValue(dict, "Compose.Create") self._LOCKED_MESSAGE = getValue(dict, "LOCKED_MESSAGE") self._LOCKED_MESSAGE_r = extractArgumentRanges(self._LOCKED_MESSAGE) self.Conversation_ContextMenuShare = getValue(dict, "Conversation.ContextMenuShare") + self._Time_MonthOfYear_m6 = getValue(dict, "Time.MonthOfYear_m6") + self._Time_MonthOfYear_m6_r = extractArgumentRanges(self._Time_MonthOfYear_m6) self._Call_GroupFormat = getValue(dict, "Call.GroupFormat") self._Call_GroupFormat_r = extractArgumentRanges(self._Call_GroupFormat) self.Forward_ChannelReadOnly = getValue(dict, "Forward.ChannelReadOnly") @@ -5584,12 +5781,12 @@ public final class PresentationStrings { self.Conversation_StatusGroupDeactivated = getValue(dict, "Conversation.StatusGroupDeactivated") self._CHAT_JOINED = getValue(dict, "CHAT_JOINED") self._CHAT_JOINED_r = extractArgumentRanges(self._CHAT_JOINED) + self._Channel_AdminLog_MessageInvitedName = getValue(dict, "Channel.AdminLog.MessageInvitedName") + self._Channel_AdminLog_MessageInvitedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedName) self.Conversation_Moderate_Ban = getValue(dict, "Conversation.Moderate.Ban") self.Group_Status = getValue(dict, "Group.Status") self.Watch_Suggestion_Absolutely = getValue(dict, "Watch.Suggestion.Absolutely") self.Conversation_InputTextPlaceholder = getValue(dict, "Conversation.InputTextPlaceholder") - self._Time_MonthOfYear_7 = getValue(dict, "Time.MonthOfYear_7") - self._Time_MonthOfYear_7_r = extractArgumentRanges(self._Time_MonthOfYear_7) self.SharedMedia_TitleAudio = getValue(dict, "SharedMedia.TitleAudio") self.TwoStepAuth_RecoveryCode = getValue(dict, "TwoStepAuth.RecoveryCode") self.SharedMedia_CategoryDocs = getValue(dict, "SharedMedia.CategoryDocs") @@ -5632,6 +5829,8 @@ public final class PresentationStrings { self.Conversation_UnsupportedMedia = getValue(dict, "Conversation.UnsupportedMedia") self._Message_ForwardedMessage = getValue(dict, "Message.ForwardedMessage") self._Message_ForwardedMessage_r = extractArgumentRanges(self._Message_ForwardedMessage) + self._Time_PreciseDate_m4 = getValue(dict, "Time.PreciseDate_m4") + self._Time_PreciseDate_m4_r = extractArgumentRanges(self._Time_PreciseDate_m4) self.Checkout_NewCard_SaveInfoEnableHelp = getValue(dict, "Checkout.NewCard.SaveInfoEnableHelp") self.Call_AudioRouteHide = getValue(dict, "Call.AudioRouteHide") self.CallSettings_OnMobile = getValue(dict, "CallSettings.OnMobile") @@ -5639,6 +5838,7 @@ public final class PresentationStrings { self.CheckoutInfo_ErrorCityInvalid = getValue(dict, "CheckoutInfo.ErrorCityInvalid") self.Profile_CreateEncryptedChatError = getValue(dict, "Profile.CreateEncryptedChatError") self.Map_LocationTitle = getValue(dict, "Map.LocationTitle") + self.Call_RateCall = getValue(dict, "Call.RateCall") self.Compose_Recipients = getValue(dict, "Compose.Recipients") self.Message_ReplyActionButtonShowReceipt = getValue(dict, "Message.ReplyActionButtonShowReceipt") self.PhotoEditor_ShadowsTool = getValue(dict, "PhotoEditor.ShadowsTool") @@ -5664,8 +5864,6 @@ public final class PresentationStrings { self.Cache_ClearCacheAlert = getValue(dict, "Cache.ClearCacheAlert") self.BroadcastLists_NoListsYet = getValue(dict, "BroadcastLists.NoListsYet") self.Settings_TabTitle = getValue(dict, "Settings.TabTitle") - self._Time_MonthOfYear_6 = getValue(dict, "Time.MonthOfYear_6") - self._Time_MonthOfYear_6_r = extractArgumentRanges(self._Time_MonthOfYear_6) self.NetworkUsageSettings_MediaAudioDataSection = getValue(dict, "NetworkUsageSettings.MediaAudioDataSection") self.GroupInfo_DeactivatedStatus = getValue(dict, "GroupInfo.DeactivatedStatus") self._CHAT_PHOTO_EDITED = getValue(dict, "CHAT_PHOTO_EDITED") @@ -5674,13 +5872,15 @@ public final class PresentationStrings { self._PrivacySettings_LastSeenEverybodyMinus = getValue(dict, "PrivacySettings.LastSeenEverybodyMinus") self._PrivacySettings_LastSeenEverybodyMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenEverybodyMinus) self.Weekday_Today = getValue(dict, "Weekday.Today") + self._Conversation_RestrictedStickersTimed = getValue(dict, "Conversation.RestrictedStickersTimed") + self._Conversation_RestrictedStickersTimed_r = extractArgumentRanges(self._Conversation_RestrictedStickersTimed) self.Login_InvalidFirstNameError = getValue(dict, "Login.InvalidFirstNameError") self._Notification_Joined = getValue(dict, "Notification.Joined") self._Notification_Joined_r = extractArgumentRanges(self._Notification_Joined) self._VideoPreview_OptionHD = getValue(dict, "VideoPreview.OptionHD") self._VideoPreview_OptionHD_r = extractArgumentRanges(self._VideoPreview_OptionHD) - self.Paint_Clear = getValue(dict, "Paint.Clear") self.TwoStepAuth_RecoveryFailed = getValue(dict, "TwoStepAuth.RecoveryFailed") + self.Paint_Clear = getValue(dict, "Paint.Clear") self._MESSAGE_AUDIO = getValue(dict, "MESSAGE_AUDIO") self._MESSAGE_AUDIO_r = extractArgumentRanges(self._MESSAGE_AUDIO) self.Checkout_PasswordEntry_Pay = getValue(dict, "Checkout.PasswordEntry.Pay") @@ -5705,6 +5905,8 @@ public final class PresentationStrings { self.Username_Placeholder = getValue(dict, "Username.Placeholder") self._Notification_PinnedDeletedMessage = getValue(dict, "Notification.PinnedDeletedMessage") self._Notification_PinnedDeletedMessage_r = extractArgumentRanges(self._Notification_PinnedDeletedMessage) + self._Time_MonthOfYear_m11 = getValue(dict, "Time.MonthOfYear_m11") + self._Time_MonthOfYear_m11_r = extractArgumentRanges(self._Time_MonthOfYear_m11) self.UserInfo_BotHelp = getValue(dict, "UserInfo.BotHelp") self.Contacts_contact = getValue(dict, "Contacts.contact") self.TwoStepAuth_PasswordSet = getValue(dict, "TwoStepAuth.PasswordSet") @@ -5733,8 +5935,6 @@ public final class PresentationStrings { self.GroupInfo_InviteLink_RevokeLink = getValue(dict, "GroupInfo.InviteLink.RevokeLink") self.Conversation_Unmute = getValue(dict, "Conversation.Unmute") self.Checkout_PaymentMethod_Title = getValue(dict, "Checkout.PaymentMethod.Title") - self._AppLanguage_LanguageSuggested = getValue(dict, "AppLanguage.LanguageSuggested") - self._AppLanguage_LanguageSuggested_r = extractArgumentRanges(self._AppLanguage_LanguageSuggested) self.Notifications_MessageNotifications = getValue(dict, "Notifications.MessageNotifications") self.ChannelMembers_WhoCanAddMembersAdminsHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAdminsHelp") self.DialogList_DeleteBotConversationConfirmation = getValue(dict, "DialogList.DeleteBotConversationConfirmation") @@ -5743,6 +5943,8 @@ public final class PresentationStrings { self._GroupInfo_InvitationLinkAccept = getValue(dict, "GroupInfo.InvitationLinkAccept") self._GroupInfo_InvitationLinkAccept_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAccept) self.Conversation_ClousStorageInfo_Description2 = getValue(dict, "Conversation.ClousStorageInfo.Description2") + self._Time_MonthOfYear_m5 = getValue(dict, "Time.MonthOfYear_m5") + self._Time_MonthOfYear_m5_r = extractArgumentRanges(self._Time_MonthOfYear_m5) self.Map_Hybrid = getValue(dict, "Map.Hybrid") self.Channel_Setup_Title = getValue(dict, "Channel.Setup.Title") self.Activity_UploadingVideo = getValue(dict, "Activity.UploadingVideo") @@ -5764,14 +5966,15 @@ public final class PresentationStrings { self.Channel_NotificationCommentsEnabled = getValue(dict, "Channel.NotificationCommentsEnabled") self.PasscodeSettings_UnlockWithTouchId = getValue(dict, "PasscodeSettings.UnlockWithTouchId") self.Contacts_AccessDeniedHelpON = getValue(dict, "Contacts.AccessDeniedHelpON") - self._Time_MonthOfYear_5 = getValue(dict, "Time.MonthOfYear_5") - self._Time_MonthOfYear_5_r = extractArgumentRanges(self._Time_MonthOfYear_5) + self.NetworkUsageSettings_ResetStats = getValue(dict, "NetworkUsageSettings.ResetStats") self._PrivacySettings_LastSeenContactsMinusPlus = getValue(dict, "PrivacySettings.LastSeenContactsMinusPlus") self._PrivacySettings_LastSeenContactsMinusPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsMinusPlus) - self.NetworkUsageSettings_ResetStats = getValue(dict, "NetworkUsageSettings.ResetStats") + self.Channel_AdminLog_EmptyMessageText = getValue(dict, "Channel.AdminLog.EmptyMessageText") self._Notification_ChannelInviter = getValue(dict, "Notification.ChannelInviter") self._Notification_ChannelInviter_r = extractArgumentRanges(self._Notification_ChannelInviter) + self.SocksProxySetup_TypeSocks = getValue(dict, "SocksProxySetup.TypeSocks") self.Profile_MessageLifetimeForever = getValue(dict, "Profile.MessageLifetimeForever") + self.SocksProxySetup_Username = getValue(dict, "SocksProxySetup.Username") self.Conversation_Edit = getValue(dict, "Conversation.Edit") self.TwoStepAuth_ResetAccountHelp = getValue(dict, "TwoStepAuth.ResetAccountHelp") self.Month_GenDecember = getValue(dict, "Month.GenDecember") @@ -5787,6 +5990,7 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_GIF_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GIF) self.Channel_AdminLogFilter_EventsEditedMessages = getValue(dict, "Channel.AdminLogFilter.EventsEditedMessages") self.Channel_Username_InvalidTooShort = getValue(dict, "Channel.Username.InvalidTooShort") + self.Conversation_ViewGroup = getValue(dict, "Conversation.ViewGroup") self.Watch_LastSeen_WithinAWeek = getValue(dict, "Watch.LastSeen.WithinAWeek") self.BlockedUsers_SelectUserTitle = getValue(dict, "BlockedUsers.SelectUserTitle") self.Profile_MessageLifetime1w = getValue(dict, "Profile.MessageLifetime1w") @@ -5795,31 +5999,38 @@ public final class PresentationStrings { self.MediaPicker_MomentsDateFormat = getValue(dict, "MediaPicker.MomentsDateFormat") self._Conversation_DownloadKilobytes = getValue(dict, "Conversation.DownloadKilobytes") self._Conversation_DownloadKilobytes_r = extractArgumentRanges(self._Conversation_DownloadKilobytes) + self._Channel_AdminLog_MessagePromotedName = getValue(dict, "Channel.AdminLog.MessagePromotedName") + self._Channel_AdminLog_MessagePromotedName_r = extractArgumentRanges(self._Channel_AdminLog_MessagePromotedName) self._Username_LinkHint = getValue(dict, "Username.LinkHint") self._Username_LinkHint_r = extractArgumentRanges(self._Username_LinkHint) + self.Group_Members_AddMemberBotErrorNotAllowed = getValue(dict, "Group.Members.AddMemberBotErrorNotAllowed") self.NetworkUsageSettings_Title = getValue(dict, "NetworkUsageSettings.Title") self.CheckoutInfo_ShippingInfoPostcodePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoPostcodePlaceholder") self.Wallpaper_Wallpaper = getValue(dict, "Wallpaper.Wallpaper") self.GroupInfo_InviteLink_RevokeAlert_Revoke = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Revoke") self.SharedMedia_TitleLink = getValue(dict, "SharedMedia.TitleLink") + self._Channel_AdminLog_MessageRestrictedName = getValue(dict, "Channel.AdminLog.MessageRestrictedName") + self._Channel_AdminLog_MessageRestrictedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedName) self.Channel_JoinChannel = getValue(dict, "Channel.JoinChannel") - self.StickerPack_Add = getValue(dict, "StickerPack.Add") - self.Group_ErrorNotMutualContact = getValue(dict, "Group.ErrorNotMutualContact") self.AccessDenied_LocationDisabled = getValue(dict, "AccessDenied.LocationDisabled") + self.Group_ErrorNotMutualContact = getValue(dict, "Group.ErrorNotMutualContact") self.Conversation_DownloadPhoto = getValue(dict, "Conversation.DownloadPhoto") - self.Login_UnknownError = getValue(dict, "Login.UnknownError") self.Presence_online = getValue(dict, "Presence.online") + self.Login_UnknownError = getValue(dict, "Login.UnknownError") self.DialogList_Title = getValue(dict, "DialogList.Title") - self.Stickers_Install = getValue(dict, "Stickers.Install") self.SearchImages_NoImagesFound = getValue(dict, "SearchImages.NoImagesFound") self._Notification_RemovedUserPhoto = getValue(dict, "Notification.RemovedUserPhoto") self._Notification_RemovedUserPhoto_r = extractArgumentRanges(self._Notification_RemovedUserPhoto) + self.Stickers_Install = getValue(dict, "Stickers.Install") self._Watch_Time_ShortTodayAt = getValue(dict, "Watch.Time.ShortTodayAt") self._Watch_Time_ShortTodayAt_r = extractArgumentRanges(self._Watch_Time_ShortTodayAt) - self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") + self.StickerPack_Add = getValue(dict, "StickerPack.Add") self.ChatSettings_Language = getValue(dict, "ChatSettings.Language") - self.AccessDenied_CameraDisabled = getValue(dict, "AccessDenied.CameraDisabled") self.Message_PinnedContactMessage = getValue(dict, "Message.PinnedContactMessage") + self.AccessDenied_CameraDisabled = getValue(dict, "AccessDenied.CameraDisabled") + self._Time_PreciseDate_m3 = getValue(dict, "Time.PreciseDate_m3") + self._Time_PreciseDate_m3_r = extractArgumentRanges(self._Time_PreciseDate_m3) + self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") self.UserInfo_Call = getValue(dict, "UserInfo.Call") self.Conversation_InputTextDisabledPlaceholder = getValue(dict, "Conversation.InputTextDisabledPlaceholder") self.Map_ForwardViaTelegram = getValue(dict, "Map.ForwardViaTelegram") @@ -5847,8 +6058,6 @@ public final class PresentationStrings { self.Message_Photo = getValue(dict, "Message.Photo") self.Conversation_ReportSpam = getValue(dict, "Conversation.ReportSpam") self.Camera_FlashAuto = getValue(dict, "Camera.FlashAuto") - self._Time_MonthOfYear_4 = getValue(dict, "Time.MonthOfYear_4") - self._Time_MonthOfYear_4_r = extractArgumentRanges(self._Time_MonthOfYear_4) self.Call_ConnectionErrorMessage = getValue(dict, "Call.ConnectionErrorMessage") self.Compose_NewChannel_AddMember = getValue(dict, "Compose.NewChannel.AddMember") self.Watch_State_Updating = getValue(dict, "Watch.State.Updating") @@ -5860,6 +6069,8 @@ public final class PresentationStrings { self.Watch_Suggestion_OnMyWay = getValue(dict, "Watch.Suggestion.OnMyWay") self.Checkout_NewCard_PaymentCard = getValue(dict, "Checkout.NewCard.PaymentCard") self.PhotoEditor_CropAspectRatioOriginal = getValue(dict, "PhotoEditor.CropAspectRatioOriginal") + self._Conversation_RestrictedInlineTimed = getValue(dict, "Conversation.RestrictedInlineTimed") + self._Conversation_RestrictedInlineTimed_r = extractArgumentRanges(self._Conversation_RestrictedInlineTimed) self.MediaPicker_MomentsDateRangeFormat = getValue(dict, "MediaPicker.MomentsDateRangeFormat") self.UserInfo_NotificationsDisabled = getValue(dict, "UserInfo.NotificationsDisabled") self._CONTACT_JOINED = getValue(dict, "CONTACT_JOINED") @@ -5868,6 +6079,8 @@ public final class PresentationStrings { self.BlockedUsers_LeavePrefix = getValue(dict, "BlockedUsers.LeavePrefix") self.NetworkUsageSettings_ResetStatsConfirmation = getValue(dict, "NetworkUsageSettings.ResetStatsConfirmation") self.Channel_EditAdmin_PermissionPostMessages = getValue(dict, "Channel.EditAdmin.PermissionPostMessages") + self._Contacts_AddPhoneNumber = getValue(dict, "Contacts.AddPhoneNumber") + self._Contacts_AddPhoneNumber_r = extractArgumentRanges(self._Contacts_AddPhoneNumber) self.DialogList_EncryptionProcessing = getValue(dict, "DialogList.EncryptionProcessing") self.Conversation_ApplyLocalization = getValue(dict, "Conversation.ApplyLocalization") self.Conversation_DeleteManyMessages = getValue(dict, "Conversation.DeleteManyMessages") @@ -5911,9 +6124,13 @@ public final class PresentationStrings { self._Channel_Management_PromotedBy_r = extractArgumentRanges(self._Channel_Management_PromotedBy) self._PrivacySettings_LastSeenNobodyPlus = getValue(dict, "PrivacySettings.LastSeenNobodyPlus") self._PrivacySettings_LastSeenNobodyPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenNobodyPlus) + self._Time_MonthOfYear_m4 = getValue(dict, "Time.MonthOfYear_m4") + self._Time_MonthOfYear_m4_r = extractArgumentRanges(self._Time_MonthOfYear_m4) self.Preview_ForwardViaTelegram = getValue(dict, "Preview.ForwardViaTelegram") self.Notifications_InAppNotificationsSounds = getValue(dict, "Notifications.InAppNotificationsSounds") self.Call_StatusRequesting = getValue(dict, "Call.StatusRequesting") + self._Channel_AdminLog_MessageRestrictedUntil = getValue(dict, "Channel.AdminLog.MessageRestrictedUntil") + self._Channel_AdminLog_MessageRestrictedUntil_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedUntil) self._CHAT_MESSAGE_CONTACT = getValue(dict, "CHAT_MESSAGE_CONTACT") self._CHAT_MESSAGE_CONTACT_r = extractArgumentRanges(self._CHAT_MESSAGE_CONTACT) self.Group_UpgradeNoticeText1 = getValue(dict, "Group.UpgradeNoticeText1") @@ -5926,10 +6143,8 @@ public final class PresentationStrings { self._Conversation_Bytes = getValue(dict, "Conversation.Bytes") self._Conversation_Bytes_r = extractArgumentRanges(self._Conversation_Bytes) self.GroupInfo_InviteLink_Help = getValue(dict, "GroupInfo.InviteLink.Help") - self._Time_MonthOfYear_3 = getValue(dict, "Time.MonthOfYear_3") - self._Time_MonthOfYear_3_r = extractArgumentRanges(self._Time_MonthOfYear_3) - self.Conversation_ContextMenuForward = getValue(dict, "Conversation.ContextMenuForward") self.Calls_Missed = getValue(dict, "Calls.Missed") + self.Conversation_ContextMenuForward = getValue(dict, "Conversation.ContextMenuForward") self.Call_StatusRinging = getValue(dict, "Call.StatusRinging") self.Invitation_JoinGroup = getValue(dict, "Invitation.JoinGroup") self.Notification_PinnedMessage = getValue(dict, "Notification.PinnedMessage") @@ -5951,9 +6166,11 @@ public final class PresentationStrings { self._PINNED_GAME_r = extractArgumentRanges(self._PINNED_GAME) self.GroupInfo_BroadcastListNamePlaceholder = getValue(dict, "GroupInfo.BroadcastListNamePlaceholder") self.Conversation_ShareBotContactConfirmation = getValue(dict, "Conversation.ShareBotContactConfirmation") + self.GroupInfo_ActionBan = getValue(dict, "GroupInfo.ActionBan") self.Login_CodeSentSms = getValue(dict, "Login.CodeSentSms") self.Conversation_ReportSpamConfirmation = getValue(dict, "Conversation.ReportSpamConfirmation") self.ChannelMembers_ChannelAdminsTitle = getValue(dict, "ChannelMembers.ChannelAdminsTitle") + self.SocksProxySetup_Credentials = getValue(dict, "SocksProxySetup.Credentials") self.CallSettings_UseLessData = getValue(dict, "CallSettings.UseLessData") self._TwoStepAuth_EnterPasswordHint = getValue(dict, "TwoStepAuth.EnterPasswordHint") self._TwoStepAuth_EnterPasswordHint_r = extractArgumentRanges(self._TwoStepAuth_EnterPasswordHint) @@ -5979,12 +6196,14 @@ public final class PresentationStrings { self._SearchImages_ImageNofM = getValue(dict, "SearchImages.ImageNofM") self._SearchImages_ImageNofM_r = extractArgumentRanges(self._SearchImages_ImageNofM) self.Channel_Username_CreatePrivateLinkHelp = getValue(dict, "Channel.Username.CreatePrivateLinkHelp") + self._Time_PreciseDate_m2 = getValue(dict, "Time.PreciseDate_m2") + self._Time_PreciseDate_m2_r = extractArgumentRanges(self._Time_PreciseDate_m2) self._FileSize_B = getValue(dict, "FileSize.B") self._FileSize_B_r = extractArgumentRanges(self._FileSize_B) - self._Target_ShareGameConfirmationGroup = getValue(dict, "Target.ShareGameConfirmationGroup") - self._Target_ShareGameConfirmationGroup_r = extractArgumentRanges(self._Target_ShareGameConfirmationGroup) self.PhotoEditor_SaturationTool = getValue(dict, "PhotoEditor.SaturationTool") self.ImagePicker_NoPhotos = getValue(dict, "ImagePicker.NoPhotos") + self._Target_ShareGameConfirmationGroup = getValue(dict, "Target.ShareGameConfirmationGroup") + self._Target_ShareGameConfirmationGroup_r = extractArgumentRanges(self._Target_ShareGameConfirmationGroup) self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") self.Channel_BanUser_BlockFor = getValue(dict, "Channel.BanUser.BlockFor") self.Preview_DeleteVideo = getValue(dict, "Preview.DeleteVideo") @@ -6034,6 +6253,7 @@ public final class PresentationStrings { self.Channel_About_Help = getValue(dict, "Channel.About.Help") self.Web_OpenExternal = getValue(dict, "Web.OpenExternal") self.UserInfo_AddContact = getValue(dict, "UserInfo.AddContact") + self.SocksProxySetup_Connection = getValue(dict, "SocksProxySetup.Connection") self.Call_EncryptionKey_Title = getValue(dict, "Call.EncryptionKey.Title") self.PhotoEditor_BlurToolLinear = getValue(dict, "PhotoEditor.BlurToolLinear") self.AuthSessions_EmptyText = getValue(dict, "AuthSessions.EmptyText") @@ -6052,28 +6272,36 @@ public final class PresentationStrings { self.TwoStepAuth_RecoveryTitle = getValue(dict, "TwoStepAuth.RecoveryTitle") self.WatchRemote_AlertOpen = getValue(dict, "WatchRemote.AlertOpen") self.ExplicitContent_AlertChannel = getValue(dict, "ExplicitContent.AlertChannel") - self.TwoStepAuth_ConfirmationText = getValue(dict, "TwoStepAuth.ConfirmationText") - self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") self._ForwardedAuthors2 = getValue(dict, "ForwardedAuthors2") self._ForwardedAuthors2_r = extractArgumentRanges(self._ForwardedAuthors2) + self.TwoStepAuth_ConfirmationText = getValue(dict, "TwoStepAuth.ConfirmationText") self.ChannelInfo_DeleteGroupConfirmation = getValue(dict, "ChannelInfo.DeleteGroupConfirmation") self.Login_SmsRequestState3 = getValue(dict, "Login.SmsRequestState3") self.Notifications_AlertTones = getValue(dict, "Notifications.AlertTones") - self.Calls_TabTitle = getValue(dict, "Calls.TabTitle") + self._Time_MonthOfYear_m10 = getValue(dict, "Time.MonthOfYear_m10") + self._Time_MonthOfYear_m10_r = extractArgumentRanges(self._Time_MonthOfYear_m10) self.Login_InfoAvatarPhoto = getValue(dict, "Login.InfoAvatarPhoto") + self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") + self.Calls_TabTitle = getValue(dict, "Calls.TabTitle") self.Contacts_MemberSearchSectionTitleChannel = getValue(dict, "Contacts.MemberSearchSectionTitleChannel") self.PhotoEditor_CurvesTool = getValue(dict, "PhotoEditor.CurvesTool") self.Preview_LoadingVideo = getValue(dict, "Preview.LoadingVideo") self.State_updating = getValue(dict, "State.updating") + self._Notification_JoinedChannel = getValue(dict, "Notification.JoinedChannel") + self._Notification_JoinedChannel_r = extractArgumentRanges(self._Notification_JoinedChannel) self.TwoStepAuth_ResetAccount = getValue(dict, "TwoStepAuth.ResetAccount") + self.GroupInfo_ActionRestrict = getValue(dict, "GroupInfo.ActionRestrict") self.Checkout_ShippingOption_Title = getValue(dict, "Checkout.ShippingOption.Title") self.Weekday_Tuesday = getValue(dict, "Weekday.Tuesday") self.Preview_Tooltip = getValue(dict, "Preview.Tooltip") self.Conversation_EncryptionProcessing = getValue(dict, "Conversation.EncryptionProcessing") + self.Weekday_ShortSunday = getValue(dict, "Weekday.ShortSunday") self._CHAT_ADD_MEMBER = getValue(dict, "CHAT_ADD_MEMBER") self._CHAT_ADD_MEMBER_r = extractArgumentRanges(self._CHAT_ADD_MEMBER) - self.Weekday_ShortSunday = getValue(dict, "Weekday.ShortSunday") + self._Channel_AdminLog_MessageKickedName = getValue(dict, "Channel.AdminLog.MessageKickedName") + self._Channel_AdminLog_MessageKickedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageKickedName) self.Month_ShortJune = getValue(dict, "Month.ShortJune") + self.Privacy_Calls_Integration = getValue(dict, "Privacy.Calls.Integration") self.Month_GenApril = getValue(dict, "Month.GenApril") self.StickerPacksSettings_ShowStickersButton = getValue(dict, "StickerPacksSettings.ShowStickersButton") self.MediaPicker_MomentsDateRangeSameMonthFormat = getValue(dict, "MediaPicker.MomentsDateRangeSameMonthFormat") @@ -6084,15 +6312,18 @@ public final class PresentationStrings { self.CallSettings_RecentCalls = getValue(dict, "CallSettings.RecentCalls") self.Conversation_Megabytes = getValue(dict, "Conversation.Megabytes") self.TwoStepAuth_FloodError = getValue(dict, "TwoStepAuth.FloodError") - self.Paint_Stickers = getValue(dict, "Paint.Stickers") self.Login_InvalidCountryCode = getValue(dict, "Login.InvalidCountryCode") + self.Paint_Stickers = getValue(dict, "Paint.Stickers") self.Privacy_Calls_AlwaysAllow_Title = getValue(dict, "Privacy.Calls.AlwaysAllow.Title") self.Username_InvalidTooShort = getValue(dict, "Username.InvalidTooShort") self.Weekday_ShortFriday = getValue(dict, "Weekday.ShortFriday") self.Conversation_ClearAll = getValue(dict, "Conversation.ClearAll") self.MediaPicker_Moments = getValue(dict, "MediaPicker.Moments") - self.Call_PhoneCallInProgressMessage = getValue(dict, "Call.PhoneCallInProgressMessage") + self.Call_ReportIncludeLog = getValue(dict, "Call.ReportIncludeLog") + self._Time_MonthOfYear_m3 = getValue(dict, "Time.MonthOfYear_m3") + self._Time_MonthOfYear_m3_r = extractArgumentRanges(self._Time_MonthOfYear_m3) self.SharedMedia_EmptyTitle = getValue(dict, "SharedMedia.EmptyTitle") + self.Call_PhoneCallInProgressMessage = getValue(dict, "Call.PhoneCallInProgressMessage") self.Checkout_Name = getValue(dict, "Checkout.Name") self.Preview_GroupPhotoTitle = getValue(dict, "Preview.GroupPhotoTitle") self._AUTH_REGION = getValue(dict, "AUTH_REGION") @@ -6102,23 +6333,25 @@ public final class PresentationStrings { self._GroupInfo_InvitationLinkAcceptChannel_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAcceptChannel) self.Conversation_EncryptionCanceled = getValue(dict, "Conversation.EncryptionCanceled") self.AccessDenied_SaveMedia = getValue(dict, "AccessDenied.SaveMedia") + self._Channel_AdminLog_MessageInvitedNameUsername = getValue(dict, "Channel.AdminLog.MessageInvitedNameUsername") + self._Channel_AdminLog_MessageInvitedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedNameUsername) self.Channel_Username_InvalidTooManyUsernames = getValue(dict, "Channel.Username.InvalidTooManyUsernames") self.Compose_GroupTokenListPlaceholder = getValue(dict, "Compose.GroupTokenListPlaceholder") self.Profile_ImageUploadError = getValue(dict, "Profile.ImageUploadError") self.Conversation_MessageDeliveryFailed = getValue(dict, "Conversation.MessageDeliveryFailed") self.Privacy_PaymentsClear_PaymentInfo = getValue(dict, "Privacy.PaymentsClear.PaymentInfo") - self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") self.Notifications_GroupNotifications = getValue(dict, "Notifications.GroupNotifications") + self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") self.CheckoutInfo_SaveInfoHelp = getValue(dict, "CheckoutInfo.SaveInfoHelp") self.StickerPacksSettings_ArchivedMasks_Info = getValue(dict, "StickerPacksSettings.ArchivedMasks.Info") self.ChannelMembers_WhoCanAddMembers_AllMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers.AllMembers") self.Channel_Edit_PrivatePublicLinkAlert = getValue(dict, "Channel.Edit.PrivatePublicLinkAlert") self.Watch_Conversation_UserInfo = getValue(dict, "Watch.Conversation.UserInfo") - self.Application_Name = getValue(dict, "Application.Name") - self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") self.Conversation_FileDropbox = getValue(dict, "Conversation.FileDropbox") self.Login_PhonePlaceholder = getValue(dict, "Login.PhonePlaceholder") self.ExplicitContent_AlertUser = getValue(dict, "ExplicitContent.AlertUser") + self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") + self.Application_Name = getValue(dict, "Application.Name") self.Profile_MessageLifetime1d = getValue(dict, "Profile.MessageLifetime1d") self.Calls_CallTabDescription = getValue(dict, "Calls.CallTabDescription") self.CheckoutInfo_ShippingInfoCityPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCityPlaceholder") @@ -6128,17 +6361,19 @@ public final class PresentationStrings { self.Channel_Setup_TypePublicHelp = getValue(dict, "Channel.Setup.TypePublicHelp") self.GroupInfo_InviteLink_RevokeAlert_Success = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Success") self.Channel_Setup_PublicNoLink = getValue(dict, "Channel.Setup.PublicNoLink") + self.Privacy_Calls_P2PHelp = getValue(dict, "Privacy.Calls.P2PHelp") self.Conversation_Info = getValue(dict, "Conversation.Info") self.ChannelInfo_InvitationLinkDoesNotExist = getValue(dict, "ChannelInfo.InvitationLinkDoesNotExist") self._Time_TodayAt = getValue(dict, "Time.TodayAt") self._Time_TodayAt_r = extractArgumentRanges(self._Time_TodayAt) self.Conversation_Processing = getValue(dict, "Conversation.Processing") - self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") - self._InstantPage_AuthorAndDateTitle_r = extractArgumentRanges(self._InstantPage_AuthorAndDateTitle) + self.Conversation_RestrictedInline = getValue(dict, "Conversation.RestrictedInline") self._Watch_LastSeen_AtDate = getValue(dict, "Watch.LastSeen.AtDate") self._Watch_LastSeen_AtDate_r = extractArgumentRanges(self._Watch_LastSeen_AtDate) self.Conversation_Location = getValue(dict, "Conversation.Location") self.DialogList_PasscodeLockHelp = getValue(dict, "DialogList.PasscodeLockHelp") + self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") + self._InstantPage_AuthorAndDateTitle_r = extractArgumentRanges(self._InstantPage_AuthorAndDateTitle) self.Channel_Management_Title = getValue(dict, "Channel.Management.Title") self.Notifications_InAppNotificationsPreview = getValue(dict, "Notifications.InAppNotificationsPreview") self.PrivacySettings_FloodControlError = getValue(dict, "PrivacySettings.FloodControlError") @@ -6154,8 +6389,8 @@ public final class PresentationStrings { self.PrivacySettings_LastSeenNobody = getValue(dict, "PrivacySettings.LastSeenNobody") self._FileSize_MB = getValue(dict, "FileSize.MB") self._FileSize_MB_r = extractArgumentRanges(self._FileSize_MB) - self.ChatSearch_SearchPlaceholder = getValue(dict, "ChatSearch.SearchPlaceholder") self.TwoStepAuth_ConfirmationAbort = getValue(dict, "TwoStepAuth.ConfirmationAbort") + self.ChatSearch_SearchPlaceholder = getValue(dict, "ChatSearch.SearchPlaceholder") self.GroupInfo_KickedStatus = getValue(dict, "GroupInfo.KickedStatus") self.TwoStepAuth_SetupPasswordConfirmFailed = getValue(dict, "TwoStepAuth.SetupPasswordConfirmFailed") self._LastSeen_YesterdayAt = getValue(dict, "LastSeen.YesterdayAt") @@ -6164,10 +6399,13 @@ public final class PresentationStrings { self.Localization_LanguageName = getValue(dict, "Localization.LanguageName") self.Map_OpenIn = getValue(dict, "Map.OpenIn") self.Message_File = getValue(dict, "Message.File") + self.Call_ReportSend = getValue(dict, "Call.ReportSend") self._Channel_AdminLog_MessageChangedGroupUsername = getValue(dict, "Channel.AdminLog.MessageChangedGroupUsername") self._Channel_AdminLog_MessageChangedGroupUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupUsername) self._CHAT_MESSAGE_GAME = getValue(dict, "CHAT_MESSAGE_GAME") self._CHAT_MESSAGE_GAME_r = extractArgumentRanges(self._CHAT_MESSAGE_GAME) + self._Time_PreciseDate_m1 = getValue(dict, "Time.PreciseDate_m1") + self._Time_PreciseDate_m1_r = extractArgumentRanges(self._Time_PreciseDate_m1) self.Month_ShortMay = getValue(dict, "Month.ShortMay") self._WelcomeScreen_Greeting = getValue(dict, "WelcomeScreen.Greeting") self._WelcomeScreen_Greeting_r = extractArgumentRanges(self._WelcomeScreen_Greeting) @@ -6201,9 +6439,9 @@ public final class PresentationStrings { self.EnterPasscode_EnterPasscode = getValue(dict, "EnterPasscode.EnterPasscode") self.Notifications_Reset = getValue(dict, "Notifications.Reset") self.GroupInfo_InvitationLinkGroupFull = getValue(dict, "GroupInfo.InvitationLinkGroupFull") + self.GoogleDrive_LogoutLogout = getValue(dict, "GoogleDrive.LogoutLogout") self._Channel_AdminLog_MessageChangedChannelUsername = getValue(dict, "Channel.AdminLog.MessageChangedChannelUsername") self._Channel_AdminLog_MessageChangedChannelUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedChannelUsername) - self.GoogleDrive_LogoutLogout = getValue(dict, "GoogleDrive.LogoutLogout") self._CHAT_MESSAGE_DOC = getValue(dict, "CHAT_MESSAGE_DOC") self._CHAT_MESSAGE_DOC_r = extractArgumentRanges(self._CHAT_MESSAGE_DOC) self.Watch_AppName = getValue(dict, "Watch.AppName") @@ -6238,10 +6476,10 @@ public final class PresentationStrings { self._DialogList_SingleRecordingVideoMessageSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingVideoMessageSuffix) self._Contacts_AccessDeniedHelpPortrait = getValue(dict, "Contacts.AccessDeniedHelpPortrait") self._Contacts_AccessDeniedHelpPortrait_r = extractArgumentRanges(self._Contacts_AccessDeniedHelpPortrait) - self.Channel_Info_BlackList = getValue(dict, "Channel.Info.BlackList") self._Checkout_LiabilityAlert = getValue(dict, "Checkout.LiabilityAlert") self._Checkout_LiabilityAlert_r = extractArgumentRanges(self._Checkout_LiabilityAlert) self.Profile_BotInfo = getValue(dict, "Profile.BotInfo") + self.Channel_Info_BlackList = getValue(dict, "Channel.Info.BlackList") self.StickerPack_RemoveStickers = getValue(dict, "StickerPack.RemoveStickers") self.Compose_NewChannel_Members = getValue(dict, "Compose.NewChannel.Members") self.Notification_Reply = getValue(dict, "Notification.Reply") @@ -6264,6 +6502,8 @@ public final class PresentationStrings { self.Message_PinnedAnimationMessage = getValue(dict, "Message.PinnedAnimationMessage") self.Checkout_ErrorPrecheckoutFailed = getValue(dict, "Checkout.ErrorPrecheckoutFailed") self.Camera_PhotoMode = getValue(dict, "Camera.PhotoMode") + self._Time_MonthOfYear_m2 = getValue(dict, "Time.MonthOfYear_m2") + self._Time_MonthOfYear_m2_r = extractArgumentRanges(self._Time_MonthOfYear_m2) self.Channel_About_Placeholder = getValue(dict, "Channel.About.Placeholder") self.Channel_About_Title = getValue(dict, "Channel.About.Title") self._MESSAGE_PHOTO = getValue(dict, "MESSAGE_PHOTO") @@ -6303,6 +6543,8 @@ public final class PresentationStrings { self.Channel_AdminLog_BanSendStickers = getValue(dict, "Channel.AdminLog.BanSendStickers") self.Common_Next = getValue(dict, "Common.Next") self.Watch_Notification_Joined = getValue(dict, "Watch.Notification.Joined") + self._Channel_AdminLog_MessageRestrictedNewSetting = getValue(dict, "Channel.AdminLog.MessageRestrictedNewSetting") + self._Channel_AdminLog_MessageRestrictedNewSetting_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNewSetting) self.ImagePicker_NoVideos = getValue(dict, "ImagePicker.NoVideos") self.GroupInfo_DeleteAndExitConfirmation = getValue(dict, "GroupInfo.DeleteAndExitConfirmation") self.ChatSettings_Cache = getValue(dict, "ChatSettings.Cache") @@ -6324,6 +6566,7 @@ public final class PresentationStrings { self.DialogList_RecentTitlePeople = getValue(dict, "DialogList.RecentTitlePeople") self.Conversation_ViewLocation = getValue(dict, "Conversation.ViewLocation") self.GroupInfo_Notifications = getValue(dict, "GroupInfo.Notifications") + self.Call_ReportPlaceholder = getValue(dict, "Call.ReportPlaceholder") self._MESSAGE_DOC = getValue(dict, "MESSAGE_DOC") self._MESSAGE_DOC_r = extractArgumentRanges(self._MESSAGE_DOC) self.Group_Username_CreatePrivateLinkHelp = getValue(dict, "Group.Username.CreatePrivateLinkHelp") @@ -6347,6 +6590,7 @@ public final class PresentationStrings { self.Conversation_MessageDialogRetry = getValue(dict, "Conversation.MessageDialogRetry") self.Watch_ChatList_NoConversationsTitle = getValue(dict, "Watch.ChatList.NoConversationsTitle") self.BlockedUsers_Title = getValue(dict, "BlockedUsers.Title") + self.ChatSettings_ConnectionType_UseSocks5 = getValue(dict, "ChatSettings.ConnectionType.UseSocks5") self.MediaPicker_MomentsDateRangeYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeYearFormat") self.Cache_ClearNone = getValue(dict, "Cache.ClearNone") self.Login_InvalidCodeError = getValue(dict, "Login.InvalidCodeError") @@ -6359,6 +6603,8 @@ public final class PresentationStrings { self.BlockedUsers_AlreadyBlocked = getValue(dict, "BlockedUsers.AlreadyBlocked") self.PrivacySettings_DeleteAccountIfAwayFor = getValue(dict, "PrivacySettings.DeleteAccountIfAwayFor") self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") + self.Channel_AdminLog_EmptyText = getValue(dict, "Channel.AdminLog.EmptyText") + self.Channel_AdminLog_EmptyFilterText = getValue(dict, "Channel.AdminLog.EmptyFilterText") self.PrivacyLastSeenSettings_CustomShareSettings_Delete = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettings.Delete") self._ENCRYPTED_MESSAGE = getValue(dict, "ENCRYPTED_MESSAGE") self._ENCRYPTED_MESSAGE_r = extractArgumentRanges(self._ENCRYPTED_MESSAGE) @@ -6375,14 +6621,16 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_ROUND = getValue(dict, "CHANNEL_MESSAGE_ROUND") self._CHANNEL_MESSAGE_ROUND_r = extractArgumentRanges(self._CHANNEL_MESSAGE_ROUND) self.GoogleDrive_FolderLoadError = getValue(dict, "GoogleDrive.FolderLoadError") + self.SocksProxySetup_Port = getValue(dict, "SocksProxySetup.Port") self.Message_VideoMessage = getValue(dict, "Message.VideoMessage") self.Conversation_ContextMenuStickerPackInfo = getValue(dict, "Conversation.ContextMenuStickerPackInfo") - self.Login_ResetAccountProtected_LimitExceeded = getValue(dict, "Login.ResetAccountProtected.LimitExceeded") self.Watch_Suggestion_TextInABit = getValue(dict, "Watch.Suggestion.TextInABit") self._CHAT_DELETE_MEMBER = getValue(dict, "CHAT_DELETE_MEMBER") self._CHAT_DELETE_MEMBER_r = extractArgumentRanges(self._CHAT_DELETE_MEMBER) + self.Login_ResetAccountProtected_LimitExceeded = getValue(dict, "Login.ResetAccountProtected.LimitExceeded") self.Conversation_EncryptedForwardingAlert = getValue(dict, "Conversation.EncryptedForwardingAlert") self.Conversation_DiscardVoiceMessageAction = getValue(dict, "Conversation.DiscardVoiceMessageAction") + self.Camera_Title = getValue(dict, "Camera.Title") self.PhotoEditor_CurvesBlue = getValue(dict, "PhotoEditor.CurvesBlue") self.Message_PinnedVideoMessage = getValue(dict, "Message.PinnedVideoMessage") self._Settings_OpenSystemPrivacySettings = getValue(dict, "Settings.OpenSystemPrivacySettings") @@ -6396,8 +6644,9 @@ public final class PresentationStrings { self._MESSAGE_ROUND_r = extractArgumentRanges(self._MESSAGE_ROUND) self.Map_Unknown = getValue(dict, "Map.Unknown") self.Wallpaper_Set = getValue(dict, "Wallpaper.Set") - self.SharedMedia_CategoryLinks = getValue(dict, "SharedMedia.CategoryLinks") self.AccessDenied_Title = getValue(dict, "AccessDenied.Title") + self.SharedMedia_CategoryLinks = getValue(dict, "SharedMedia.CategoryLinks") + self.Localization_LanguageOther = getValue(dict, "Localization.LanguageOther") self.Conversation_ClearAllConfirmation = getValue(dict, "Conversation.ClearAllConfirmation") self.TwoStepAuth_EmailSkipAlert = getValue(dict, "TwoStepAuth.EmailSkipAlert") self.ChatSettings_Stickers = getValue(dict, "ChatSettings.Stickers") @@ -6443,9 +6692,12 @@ public final class PresentationStrings { self.SearchImages_ErrorDownloadingImage = getValue(dict, "SearchImages.ErrorDownloadingImage") self._PINNED_GIF = getValue(dict, "PINNED_GIF") self._PINNED_GIF_r = extractArgumentRanges(self._PINNED_GIF) + self.Channel_EditAdmin_CannotEdit = getValue(dict, "Channel.EditAdmin.CannotEdit") self.Profile_PhonebookAccessDisabled = getValue(dict, "Profile.PhonebookAccessDisabled") self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") self.BlockedUsers_Unblock = getValue(dict, "BlockedUsers.Unblock") + self._Time_MonthOfYear_m1 = getValue(dict, "Time.MonthOfYear_m1") + self._Time_MonthOfYear_m1_r = extractArgumentRanges(self._Time_MonthOfYear_m1) self.Conversation_ViewFile = getValue(dict, "Conversation.ViewFile") self.Notifications_GroupNotificationsAlert = getValue(dict, "Notifications.GroupNotificationsAlert") self.Paint_Masks = getValue(dict, "Paint.Masks") @@ -6457,23 +6709,25 @@ public final class PresentationStrings { self._FileSize_KB = getValue(dict, "FileSize.KB") self._FileSize_KB_r = extractArgumentRanges(self._FileSize_KB) self.Watch_GroupInfo_Title = getValue(dict, "Watch.GroupInfo.Title") + self.Channel_AdminLog_EmptyTitle = getValue(dict, "Channel.AdminLog.EmptyTitle") self.PhotoEditor_Set = getValue(dict, "PhotoEditor.Set") self._Notification_Invited = getValue(dict, "Notification.Invited") self._Notification_Invited_r = extractArgumentRanges(self._Notification_Invited) self.Watch_AuthRequired = getValue(dict, "Watch.AuthRequired") self.Conversation_EncryptedDescription1 = getValue(dict, "Conversation.EncryptedDescription1") self.AppleWatch_ReplyPresets = getValue(dict, "AppleWatch.ReplyPresets") + self.Channel_Members_AddAdminErrorNotAMember = getValue(dict, "Channel.Members.AddAdminErrorNotAMember") self.Conversation_EncryptedDescription2 = getValue(dict, "Conversation.EncryptedDescription2") - self.NetworkUsageSettings_MediaVideoDataSection = getValue(dict, "NetworkUsageSettings.MediaVideoDataSection") self.Paint_Edit = getValue(dict, "Paint.Edit") + self.NetworkUsageSettings_MediaVideoDataSection = getValue(dict, "NetworkUsageSettings.MediaVideoDataSection") self.Conversation_EncryptedDescription3 = getValue(dict, "Conversation.EncryptedDescription3") self.Login_CodeFloodError = getValue(dict, "Login.CodeFloodError") self._Call_EncryptionKey_Description = getValue(dict, "Call.EncryptionKey.Description") self._Call_EncryptionKey_Description_r = extractArgumentRanges(self._Call_EncryptionKey_Description) self.Conversation_EncryptedDescription4 = getValue(dict, "Conversation.EncryptedDescription4") self.AppleWatch_Title = getValue(dict, "AppleWatch.Title") - self.Conversation_StatusTyping = getValue(dict, "Conversation.StatusTyping") self.Contacts_AccessDeniedError = getValue(dict, "Contacts.AccessDeniedError") + self.Conversation_StatusTyping = getValue(dict, "Conversation.StatusTyping") self.GoogleDrive_LoadErrorTitle = getValue(dict, "GoogleDrive.LoadErrorTitle") self.Share_Title = getValue(dict, "Share.Title") self.Map_Send = getValue(dict, "Map.Send") @@ -6500,6 +6754,7 @@ public final class PresentationStrings { self.DialogList_SearchSectionMessages = getValue(dict, "DialogList.SearchSectionMessages") self._Profile_ShareBotGroupFormat = getValue(dict, "Profile.ShareBotGroupFormat") self._Profile_ShareBotGroupFormat_r = extractArgumentRanges(self._Profile_ShareBotGroupFormat) + self.Call_ReportIncludeLogDescription = getValue(dict, "Call.ReportIncludeLogDescription") self.Preview_DeleteGif = getValue(dict, "Preview.DeleteGif") self.Weekday_Saturday = getValue(dict, "Weekday.Saturday") self.UserInfo_DeleteContact = getValue(dict, "UserInfo.DeleteContact") @@ -6517,6 +6772,8 @@ public final class PresentationStrings { self.Channel_AdminLog_CanPinMessages = getValue(dict, "Channel.AdminLog.CanPinMessages") self.KeyCommand_NewMessage = getValue(dict, "KeyCommand.NewMessage") self.Compose_NewBroadcastButton = getValue(dict, "Compose.NewBroadcastButton") + self._Time_PreciseDate_m12 = getValue(dict, "Time.PreciseDate_m12") + self._Time_PreciseDate_m12_r = extractArgumentRanges(self._Time_PreciseDate_m12) self.NetworkUsageSettings_TotalSection = getValue(dict, "NetworkUsageSettings.TotalSection") self._PINNED_AUDIO = getValue(dict, "PINNED_AUDIO") self._PINNED_AUDIO_r = extractArgumentRanges(self._PINNED_AUDIO) @@ -6561,6 +6818,7 @@ public final class PresentationStrings { self.Notifications_GroupNotificationsHelp = getValue(dict, "Notifications.GroupNotificationsHelp") self.PhotoEditor_CropAspectRatioSquare = getValue(dict, "PhotoEditor.CropAspectRatioSquare") self.Notification_CallOutgoing = getValue(dict, "Notification.CallOutgoing") + self.SocksProxySetup_Password = getValue(dict, "SocksProxySetup.Password") self.Weekday_ShortMonday = getValue(dict, "Weekday.ShortMonday") self.Channel_Edit_AboutItem = getValue(dict, "Channel.Edit.AboutItem") self.Checkout_Receipt_Title = getValue(dict, "Checkout.Receipt.Title") @@ -6584,17 +6842,19 @@ public final class PresentationStrings { self._Profile_ShareContactPersonFormat_r = extractArgumentRanges(self._Profile_ShareContactPersonFormat) self._CHANNEL_MESSAGE_GEO = getValue(dict, "CHANNEL_MESSAGE_GEO") self._CHANNEL_MESSAGE_GEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GEO) + self.Contacts_PhoneNumber = getValue(dict, "Contacts.PhoneNumber") self.Group_Info_AdminLog = getValue(dict, "Group.Info.AdminLog") + self.Channel_AdminLogFilter_ChannelEventsInfo = getValue(dict, "Channel.AdminLogFilter.ChannelEventsInfo") self.StickerPacksSettings_FeaturedPacks = getValue(dict, "StickerPacksSettings.FeaturedPacks") self.Month_GenAugust = getValue(dict, "Month.GenAugust") self.Channel_Username_CreatePublicLinkHelp = getValue(dict, "Channel.Username.CreatePublicLinkHelp") self.StickerPack_Send = getValue(dict, "StickerPack.Send") self.Watch_Suggestion_HoldOn = getValue(dict, "Watch.Suggestion.HoldOn") - self.StickerSettings_MaskContextInfo = getValue(dict, "StickerSettings.MaskContextInfo") self.AttachmentMenu_ImageSearch = getValue(dict, "AttachmentMenu.ImageSearch") + self.PasscodeSettings_EncryptData = getValue(dict, "PasscodeSettings.EncryptData") self._PINNED_GEO = getValue(dict, "PINNED_GEO") self._PINNED_GEO_r = extractArgumentRanges(self._PINNED_GEO) - self.PasscodeSettings_EncryptData = getValue(dict, "PasscodeSettings.EncryptData") + self.StickerSettings_MaskContextInfo = getValue(dict, "StickerSettings.MaskContextInfo") self.Notification_CallCanceled = getValue(dict, "Notification.CallCanceled") self.Common_NotNow = getValue(dict, "Common.NotNow") self.PasscodeSettings_Title = getValue(dict, "PasscodeSettings.Title") @@ -6610,6 +6870,7 @@ public final class PresentationStrings { self.Channel_EditAdmin_PermissionBanUsers = getValue(dict, "Channel.EditAdmin.PermissionBanUsers") self.Wallpaper_PhotoLibrary = getValue(dict, "Wallpaper.PhotoLibrary") self.Settings_About = getValue(dict, "Settings.About") + self.Privacy_Calls_IntegrationHelp = getValue(dict, "Privacy.Calls.IntegrationHelp") self._CHAT_LEFT = getValue(dict, "CHAT_LEFT") self._CHAT_LEFT_r = extractArgumentRanges(self._CHAT_LEFT) self.LoginPassword_ForgotPassword = getValue(dict, "LoginPassword.ForgotPassword") @@ -6630,7 +6891,10 @@ public final class PresentationStrings { self._Channel_MessageTitleUpdated = getValue(dict, "Channel.MessageTitleUpdated") self._Channel_MessageTitleUpdated_r = extractArgumentRanges(self._Channel_MessageTitleUpdated) self.Call_CallAgain = getValue(dict, "Call.CallAgain") + self.Message_VideoExpired = getValue(dict, "Message.VideoExpired") self.TwoStepAuth_RecoveryCodeHelp = getValue(dict, "TwoStepAuth.RecoveryCodeHelp") + self._Channel_AdminLog_MessagePromotedNameUsername = getValue(dict, "Channel.AdminLog.MessagePromotedNameUsername") + self._Channel_AdminLog_MessagePromotedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessagePromotedNameUsername) self.UserInfo_SendMessage = getValue(dict, "UserInfo.SendMessage") self._Channel_Username_LinkHint = getValue(dict, "Channel.Username.LinkHint") self._Channel_Username_LinkHint_r = extractArgumentRanges(self._Channel_Username_LinkHint) @@ -6643,14 +6907,14 @@ public final class PresentationStrings { self.Channel_Moderator_Title = getValue(dict, "Channel.Moderator.Title") self.Message_PinnedPhotoMessage = getValue(dict, "Message.PinnedPhotoMessage") self.Notification_SecretChatScreenshot = getValue(dict, "Notification.SecretChatScreenshot") - self._Conversation_DeleteMessagesFor = getValue(dict, "Conversation.DeleteMessagesFor") - self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) self.Activity_UploadingDocument = getValue(dict, "Activity.UploadingDocument") + self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") self.Watch_ChatList_NoConversationsText = getValue(dict, "Watch.ChatList.NoConversationsText") self.ReportPeer_AlertSuccess = getValue(dict, "ReportPeer.AlertSuccess") self.Tour_Text4 = getValue(dict, "Tour.Text4") self.Channel_Info_Description = getValue(dict, "Channel.Info.Description") - self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") + self._Conversation_DeleteMessagesFor = getValue(dict, "Conversation.DeleteMessagesFor") + self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) self.MessageTimer_Title = getValue(dict, "MessageTimer.Title") self.Watch_Compose_Send = getValue(dict, "Watch.Compose.Send") self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress") @@ -6660,8 +6924,8 @@ public final class PresentationStrings { self.Channel_EditAdmin_PermissionChangeInfo = getValue(dict, "Channel.EditAdmin.PermissionChangeInfo") self.Notifications_ResetAllNotificationsHelp = getValue(dict, "Notifications.ResetAllNotificationsHelp") self.DialogList_EncryptionRejected = getValue(dict, "DialogList.EncryptionRejected") - self.AccessDenied_CameraRestricted = getValue(dict, "AccessDenied.CameraRestricted") self.Target_InviteToGroupErrorAlreadyInvited = getValue(dict, "Target.InviteToGroupErrorAlreadyInvited") + self.AccessDenied_CameraRestricted = getValue(dict, "AccessDenied.CameraRestricted") self.Watch_Message_ForwardedFrom = getValue(dict, "Watch.Message.ForwardedFrom") self.Channel_AboutItem = getValue(dict, "Channel.AboutItem") self.PhotoEditor_CurvesGreen = getValue(dict, "PhotoEditor.CurvesGreen") @@ -6679,6 +6943,8 @@ public final class PresentationStrings { self.Channel_AdminLog_CanStartCalls = getValue(dict, "Channel.AdminLog.CanStartCalls") self._Login_ResetAccountProtected_Text = getValue(dict, "Login.ResetAccountProtected.Text") self._Login_ResetAccountProtected_Text_r = extractArgumentRanges(self._Login_ResetAccountProtected_Text) + self._Channel_AdminLog_EmptyFilterQueryText = getValue(dict, "Channel.AdminLog.EmptyFilterQueryText") + self._Channel_AdminLog_EmptyFilterQueryText_r = extractArgumentRanges(self._Channel_AdminLog_EmptyFilterQueryText) self.Camera_TapAndHoldForVideo = getValue(dict, "Camera.TapAndHoldForVideo") self.Bot_DescriptionTitle = getValue(dict, "Bot.DescriptionTitle") self.FeaturedStickerPacks_Title = getValue(dict, "FeaturedStickerPacks.Title") @@ -6734,10 +7000,11 @@ public final class PresentationStrings { self.Conversation_BroadcastTitle = getValue(dict, "Conversation.BroadcastTitle") self.Username_Help = getValue(dict, "Username.Help") self.StickerSettings_ContextHide = getValue(dict, "StickerSettings.ContextHide") - self.Weekday_Sunday = getValue(dict, "Weekday.Sunday") self.Preview_LoadingImage = getValue(dict, "Preview.LoadingImage") + self.Weekday_Sunday = getValue(dict, "Weekday.Sunday") self._Conversation_DownloadProgressKilobytes = getValue(dict, "Conversation.DownloadProgressKilobytes") self._Conversation_DownloadProgressKilobytes_r = extractArgumentRanges(self._Conversation_DownloadProgressKilobytes) + self.Contacts_ShareTelegram = getValue(dict, "Contacts.ShareTelegram") self.Settings_ChatBackground = getValue(dict, "Settings.ChatBackground") self._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) @@ -6757,18 +7024,18 @@ public final class PresentationStrings { self._MessageTimer_ShortSeconds_few = getValueWithForm(dict, "MessageTimer.ShortSeconds", .few) self._MessageTimer_ShortSeconds_many = getValueWithForm(dict, "MessageTimer.ShortSeconds", .many) self._MessageTimer_ShortSeconds_other = getValueWithForm(dict, "MessageTimer.ShortSeconds", .other) - self._Notification_GameScoreSimple_zero = getValueWithForm(dict, "Notification.GameScoreSimple", .zero) - self._Notification_GameScoreSimple_one = getValueWithForm(dict, "Notification.GameScoreSimple", .one) - self._Notification_GameScoreSimple_two = getValueWithForm(dict, "Notification.GameScoreSimple", .two) - self._Notification_GameScoreSimple_few = getValueWithForm(dict, "Notification.GameScoreSimple", .few) - self._Notification_GameScoreSimple_many = getValueWithForm(dict, "Notification.GameScoreSimple", .many) - self._Notification_GameScoreSimple_other = getValueWithForm(dict, "Notification.GameScoreSimple", .other) self._Notification_GameScoreExtended_zero = getValueWithForm(dict, "Notification.GameScoreExtended", .zero) self._Notification_GameScoreExtended_one = getValueWithForm(dict, "Notification.GameScoreExtended", .one) self._Notification_GameScoreExtended_two = getValueWithForm(dict, "Notification.GameScoreExtended", .two) self._Notification_GameScoreExtended_few = getValueWithForm(dict, "Notification.GameScoreExtended", .few) self._Notification_GameScoreExtended_many = getValueWithForm(dict, "Notification.GameScoreExtended", .many) self._Notification_GameScoreExtended_other = getValueWithForm(dict, "Notification.GameScoreExtended", .other) + self._Notification_GameScoreSimple_zero = getValueWithForm(dict, "Notification.GameScoreSimple", .zero) + self._Notification_GameScoreSimple_one = getValueWithForm(dict, "Notification.GameScoreSimple", .one) + self._Notification_GameScoreSimple_two = getValueWithForm(dict, "Notification.GameScoreSimple", .two) + self._Notification_GameScoreSimple_few = getValueWithForm(dict, "Notification.GameScoreSimple", .few) + self._Notification_GameScoreSimple_many = getValueWithForm(dict, "Notification.GameScoreSimple", .many) + self._Notification_GameScoreSimple_other = getValueWithForm(dict, "Notification.GameScoreSimple", .other) self._PasscodeSettings_FailedAttempts_zero = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .zero) self._PasscodeSettings_FailedAttempts_one = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .one) self._PasscodeSettings_FailedAttempts_two = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .two) @@ -6823,12 +7090,6 @@ public final class PresentationStrings { self._Call_ShortSeconds_few = getValueWithForm(dict, "Call.ShortSeconds", .few) self._Call_ShortSeconds_many = getValueWithForm(dict, "Call.ShortSeconds", .many) self._Call_ShortSeconds_other = getValueWithForm(dict, "Call.ShortSeconds", .other) - self._Time_PreciseDate_zero = getValueWithForm(dict, "Time.PreciseDate", .zero) - self._Time_PreciseDate_one = getValueWithForm(dict, "Time.PreciseDate", .one) - self._Time_PreciseDate_two = getValueWithForm(dict, "Time.PreciseDate", .two) - self._Time_PreciseDate_few = getValueWithForm(dict, "Time.PreciseDate", .few) - self._Time_PreciseDate_many = getValueWithForm(dict, "Time.PreciseDate", .many) - self._Time_PreciseDate_other = getValueWithForm(dict, "Time.PreciseDate", .other) self._SharedMedia_File_zero = getValueWithForm(dict, "SharedMedia.File", .zero) self._SharedMedia_File_one = getValueWithForm(dict, "SharedMedia.File", .one) self._SharedMedia_File_two = getValueWithForm(dict, "SharedMedia.File", .two) @@ -6901,18 +7162,18 @@ public final class PresentationStrings { self._StickerPack_StickerCount_few = getValueWithForm(dict, "StickerPack.StickerCount", .few) self._StickerPack_StickerCount_many = getValueWithForm(dict, "StickerPack.StickerCount", .many) self._StickerPack_StickerCount_other = getValueWithForm(dict, "StickerPack.StickerCount", .other) - self._SharedMedia_Video_zero = getValueWithForm(dict, "SharedMedia.Video", .zero) - self._SharedMedia_Video_one = getValueWithForm(dict, "SharedMedia.Video", .one) - self._SharedMedia_Video_two = getValueWithForm(dict, "SharedMedia.Video", .two) - self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) - self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) - self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) self._ForwardedAuthorsOthers_zero = getValueWithForm(dict, "ForwardedAuthorsOthers", .zero) self._ForwardedAuthorsOthers_one = getValueWithForm(dict, "ForwardedAuthorsOthers", .one) self._ForwardedAuthorsOthers_two = getValueWithForm(dict, "ForwardedAuthorsOthers", .two) self._ForwardedAuthorsOthers_few = getValueWithForm(dict, "ForwardedAuthorsOthers", .few) self._ForwardedAuthorsOthers_many = getValueWithForm(dict, "ForwardedAuthorsOthers", .many) self._ForwardedAuthorsOthers_other = getValueWithForm(dict, "ForwardedAuthorsOthers", .other) + self._SharedMedia_Video_zero = getValueWithForm(dict, "SharedMedia.Video", .zero) + self._SharedMedia_Video_one = getValueWithForm(dict, "SharedMedia.Video", .one) + self._SharedMedia_Video_two = getValueWithForm(dict, "SharedMedia.Video", .two) + self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) + self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) + self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) self._MuteFor_Minutes_zero = getValueWithForm(dict, "MuteFor.Minutes", .zero) self._MuteFor_Minutes_one = getValueWithForm(dict, "MuteFor.Minutes", .one) self._MuteFor_Minutes_two = getValueWithForm(dict, "MuteFor.Minutes", .two) @@ -6943,18 +7204,18 @@ public final class PresentationStrings { self._Channel_NotificationComments_few = getValueWithForm(dict, "Channel.NotificationComments", .few) self._Channel_NotificationComments_many = getValueWithForm(dict, "Channel.NotificationComments", .many) self._Channel_NotificationComments_other = getValueWithForm(dict, "Channel.NotificationComments", .other) - self._UserCount_zero = getValueWithForm(dict, "UserCount", .zero) - self._UserCount_one = getValueWithForm(dict, "UserCount", .one) - self._UserCount_two = getValueWithForm(dict, "UserCount", .two) - self._UserCount_few = getValueWithForm(dict, "UserCount", .few) - self._UserCount_many = getValueWithForm(dict, "UserCount", .many) - self._UserCount_other = getValueWithForm(dict, "UserCount", .other) self._ForwardedGifs_zero = getValueWithForm(dict, "ForwardedGifs", .zero) self._ForwardedGifs_one = getValueWithForm(dict, "ForwardedGifs", .one) self._ForwardedGifs_two = getValueWithForm(dict, "ForwardedGifs", .two) self._ForwardedGifs_few = getValueWithForm(dict, "ForwardedGifs", .few) self._ForwardedGifs_many = getValueWithForm(dict, "ForwardedGifs", .many) self._ForwardedGifs_other = getValueWithForm(dict, "ForwardedGifs", .other) + self._UserCount_zero = getValueWithForm(dict, "UserCount", .zero) + self._UserCount_one = getValueWithForm(dict, "UserCount", .one) + self._UserCount_two = getValueWithForm(dict, "UserCount", .two) + self._UserCount_few = getValueWithForm(dict, "UserCount", .few) + self._UserCount_many = getValueWithForm(dict, "UserCount", .many) + self._UserCount_other = getValueWithForm(dict, "UserCount", .other) self._MessageTimer_ShortHours_zero = getValueWithForm(dict, "MessageTimer.ShortHours", .zero) self._MessageTimer_ShortHours_one = getValueWithForm(dict, "MessageTimer.ShortHours", .one) self._MessageTimer_ShortHours_two = getValueWithForm(dict, "MessageTimer.ShortHours", .two) @@ -6979,18 +7240,18 @@ public final class PresentationStrings { self._AttachmentMenu_SendPhoto_few = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .few) self._AttachmentMenu_SendPhoto_many = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .many) self._AttachmentMenu_SendPhoto_other = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .other) + self._LastSeen_MinutesAgo_zero = getValueWithForm(dict, "LastSeen.MinutesAgo", .zero) + self._LastSeen_MinutesAgo_one = getValueWithForm(dict, "LastSeen.MinutesAgo", .one) + self._LastSeen_MinutesAgo_two = getValueWithForm(dict, "LastSeen.MinutesAgo", .two) + self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) + self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) + self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) self._Conversation_StatusRecipients_zero = getValueWithForm(dict, "Conversation.StatusRecipients", .zero) self._Conversation_StatusRecipients_one = getValueWithForm(dict, "Conversation.StatusRecipients", .one) self._Conversation_StatusRecipients_two = getValueWithForm(dict, "Conversation.StatusRecipients", .two) self._Conversation_StatusRecipients_few = getValueWithForm(dict, "Conversation.StatusRecipients", .few) self._Conversation_StatusRecipients_many = getValueWithForm(dict, "Conversation.StatusRecipients", .many) self._Conversation_StatusRecipients_other = getValueWithForm(dict, "Conversation.StatusRecipients", .other) - self._Channel_Management_LabelRights_zero = getValueWithForm(dict, "Channel.Management.LabelRights", .zero) - self._Channel_Management_LabelRights_one = getValueWithForm(dict, "Channel.Management.LabelRights", .one) - self._Channel_Management_LabelRights_two = getValueWithForm(dict, "Channel.Management.LabelRights", .two) - self._Channel_Management_LabelRights_few = getValueWithForm(dict, "Channel.Management.LabelRights", .few) - self._Channel_Management_LabelRights_many = getValueWithForm(dict, "Channel.Management.LabelRights", .many) - self._Channel_Management_LabelRights_other = getValueWithForm(dict, "Channel.Management.LabelRights", .other) self._ServiceMessage_GameScoreSelfSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .zero) self._ServiceMessage_GameScoreSelfSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .one) self._ServiceMessage_GameScoreSelfSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .two) @@ -7015,12 +7276,12 @@ public final class PresentationStrings { self._StickerPack_AddMaskCount_few = getValueWithForm(dict, "StickerPack.AddMaskCount", .few) self._StickerPack_AddMaskCount_many = getValueWithForm(dict, "StickerPack.AddMaskCount", .many) self._StickerPack_AddMaskCount_other = getValueWithForm(dict, "StickerPack.AddMaskCount", .other) - self._LastSeen_MinutesAgo_zero = getValueWithForm(dict, "LastSeen.MinutesAgo", .zero) - self._LastSeen_MinutesAgo_one = getValueWithForm(dict, "LastSeen.MinutesAgo", .one) - self._LastSeen_MinutesAgo_two = getValueWithForm(dict, "LastSeen.MinutesAgo", .two) - self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) - self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) - self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) + self._Channel_Management_LabelRights_zero = getValueWithForm(dict, "Channel.Management.LabelRights", .zero) + self._Channel_Management_LabelRights_one = getValueWithForm(dict, "Channel.Management.LabelRights", .one) + self._Channel_Management_LabelRights_two = getValueWithForm(dict, "Channel.Management.LabelRights", .two) + self._Channel_Management_LabelRights_few = getValueWithForm(dict, "Channel.Management.LabelRights", .few) + self._Channel_Management_LabelRights_many = getValueWithForm(dict, "Channel.Management.LabelRights", .many) + self._Channel_Management_LabelRights_other = getValueWithForm(dict, "Channel.Management.LabelRights", .other) self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) @@ -7081,12 +7342,12 @@ public final class PresentationStrings { self._SharedMedia_DeleteItemsConfirmation_few = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .few) self._SharedMedia_DeleteItemsConfirmation_many = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .many) self._SharedMedia_DeleteItemsConfirmation_other = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .other) - self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) - self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) - self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) - self._Watch_LastSeen_MinutesAgo_few = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .few) - self._Watch_LastSeen_MinutesAgo_many = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .many) - self._Watch_LastSeen_MinutesAgo_other = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .other) + self._ForwardedVideos_zero = getValueWithForm(dict, "ForwardedVideos", .zero) + self._ForwardedVideos_one = getValueWithForm(dict, "ForwardedVideos", .one) + self._ForwardedVideos_two = getValueWithForm(dict, "ForwardedVideos", .two) + self._ForwardedVideos_few = getValueWithForm(dict, "ForwardedVideos", .few) + self._ForwardedVideos_many = getValueWithForm(dict, "ForwardedVideos", .many) + self._ForwardedVideos_other = getValueWithForm(dict, "ForwardedVideos", .other) self._ForwardedMessages_zero = getValueWithForm(dict, "ForwardedMessages", .zero) self._ForwardedMessages_one = getValueWithForm(dict, "ForwardedMessages", .one) self._ForwardedMessages_two = getValueWithForm(dict, "ForwardedMessages", .two) @@ -7099,12 +7360,12 @@ public final class PresentationStrings { self._SharedMedia_ItemsSelected_few = getValueWithForm(dict, "SharedMedia.ItemsSelected", .few) self._SharedMedia_ItemsSelected_many = getValueWithForm(dict, "SharedMedia.ItemsSelected", .many) self._SharedMedia_ItemsSelected_other = getValueWithForm(dict, "SharedMedia.ItemsSelected", .other) - self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) - self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) - self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) - self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) - self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) - self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) + self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) + self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) + self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) + self._Watch_LastSeen_MinutesAgo_few = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .few) + self._Watch_LastSeen_MinutesAgo_many = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .many) + self._Watch_LastSeen_MinutesAgo_other = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .other) self._MessageTimer_Years_zero = getValueWithForm(dict, "MessageTimer.Years", .zero) self._MessageTimer_Years_one = getValueWithForm(dict, "MessageTimer.Years", .one) self._MessageTimer_Years_two = getValueWithForm(dict, "MessageTimer.Years", .two) @@ -7117,12 +7378,12 @@ public final class PresentationStrings { self._Map_ETAMinutes_few = getValueWithForm(dict, "Map.ETAMinutes", .few) self._Map_ETAMinutes_many = getValueWithForm(dict, "Map.ETAMinutes", .many) self._Map_ETAMinutes_other = getValueWithForm(dict, "Map.ETAMinutes", .other) - self._ForwardedVideos_zero = getValueWithForm(dict, "ForwardedVideos", .zero) - self._ForwardedVideos_one = getValueWithForm(dict, "ForwardedVideos", .one) - self._ForwardedVideos_two = getValueWithForm(dict, "ForwardedVideos", .two) - self._ForwardedVideos_few = getValueWithForm(dict, "ForwardedVideos", .few) - self._ForwardedVideos_many = getValueWithForm(dict, "ForwardedVideos", .many) - self._ForwardedVideos_other = getValueWithForm(dict, "ForwardedVideos", .other) + self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) + self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) + self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) + self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) + self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) + self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) self._Notification_GameScoreSelfSimple_zero = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .zero) self._Notification_GameScoreSelfSimple_one = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .one) self._Notification_GameScoreSelfSimple_two = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .two) @@ -7159,12 +7420,6 @@ public final class PresentationStrings { self._AttachmentMenu_SendItem_few = getValueWithForm(dict, "AttachmentMenu.SendItem", .few) self._AttachmentMenu_SendItem_many = getValueWithForm(dict, "AttachmentMenu.SendItem", .many) self._AttachmentMenu_SendItem_other = getValueWithForm(dict, "AttachmentMenu.SendItem", .other) - self._Time_MonthOfYear_zero = getValueWithForm(dict, "Time.MonthOfYear", .zero) - self._Time_MonthOfYear_one = getValueWithForm(dict, "Time.MonthOfYear", .one) - self._Time_MonthOfYear_two = getValueWithForm(dict, "Time.MonthOfYear", .two) - self._Time_MonthOfYear_few = getValueWithForm(dict, "Time.MonthOfYear", .few) - self._Time_MonthOfYear_many = getValueWithForm(dict, "Time.MonthOfYear", .many) - self._Time_MonthOfYear_other = getValueWithForm(dict, "Time.MonthOfYear", .other) self._Watch_UserInfo_Mute_zero = getValueWithForm(dict, "Watch.UserInfo.Mute", .zero) self._Watch_UserInfo_Mute_one = getValueWithForm(dict, "Watch.UserInfo.Mute", .one) self._Watch_UserInfo_Mute_two = getValueWithForm(dict, "Watch.UserInfo.Mute", .two) @@ -7195,18 +7450,18 @@ public final class PresentationStrings { self._ForwardedLocations_few = getValueWithForm(dict, "ForwardedLocations", .few) self._ForwardedLocations_many = getValueWithForm(dict, "ForwardedLocations", .many) self._ForwardedLocations_other = getValueWithForm(dict, "ForwardedLocations", .other) - self._MessageTimer_ShortWeeks_zero = getValueWithForm(dict, "MessageTimer.ShortWeeks", .zero) - self._MessageTimer_ShortWeeks_one = getValueWithForm(dict, "MessageTimer.ShortWeeks", .one) - self._MessageTimer_ShortWeeks_two = getValueWithForm(dict, "MessageTimer.ShortWeeks", .two) - self._MessageTimer_ShortWeeks_few = getValueWithForm(dict, "MessageTimer.ShortWeeks", .few) - self._MessageTimer_ShortWeeks_many = getValueWithForm(dict, "MessageTimer.ShortWeeks", .many) - self._MessageTimer_ShortWeeks_other = getValueWithForm(dict, "MessageTimer.ShortWeeks", .other) self._MessageTimer_Minutes_zero = getValueWithForm(dict, "MessageTimer.Minutes", .zero) self._MessageTimer_Minutes_one = getValueWithForm(dict, "MessageTimer.Minutes", .one) self._MessageTimer_Minutes_two = getValueWithForm(dict, "MessageTimer.Minutes", .two) self._MessageTimer_Minutes_few = getValueWithForm(dict, "MessageTimer.Minutes", .few) self._MessageTimer_Minutes_many = getValueWithForm(dict, "MessageTimer.Minutes", .many) self._MessageTimer_Minutes_other = getValueWithForm(dict, "MessageTimer.Minutes", .other) + self._MessageTimer_ShortWeeks_zero = getValueWithForm(dict, "MessageTimer.ShortWeeks", .zero) + self._MessageTimer_ShortWeeks_one = getValueWithForm(dict, "MessageTimer.ShortWeeks", .one) + self._MessageTimer_ShortWeeks_two = getValueWithForm(dict, "MessageTimer.ShortWeeks", .two) + self._MessageTimer_ShortWeeks_few = getValueWithForm(dict, "MessageTimer.ShortWeeks", .few) + self._MessageTimer_ShortWeeks_many = getValueWithForm(dict, "MessageTimer.ShortWeeks", .many) + self._MessageTimer_ShortWeeks_other = getValueWithForm(dict, "MessageTimer.ShortWeeks", .other) self._MessageTimer_Months_zero = getValueWithForm(dict, "MessageTimer.Months", .zero) self._MessageTimer_Months_one = getValueWithForm(dict, "MessageTimer.Months", .one) self._MessageTimer_Months_two = getValueWithForm(dict, "MessageTimer.Months", .two) diff --git a/TelegramUI/PresentationSurfaceLevels.swift b/TelegramUI/PresentationSurfaceLevels.swift new file mode 100644 index 0000000000..a0b56a461d --- /dev/null +++ b/TelegramUI/PresentationSurfaceLevels.swift @@ -0,0 +1,9 @@ +import Foundation +import Display + +public extension PresentationSurfaceLevel { + static let root = PresentationSurfaceLevel(rawValue: 0) + static let calls = PresentationSurfaceLevel(rawValue: 1) + static let overlayMedia = PresentationSurfaceLevel(rawValue: 2) + static let notifications = PresentationSurfaceLevel(rawValue: 3) +} diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 837bf888bb..757a0eb769 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -507,7 +507,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign (controller?.navigationController as? NavigationController)?.pushViewController(c, animated: false) } presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } return controller diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index 5fa626db53..65d80e5b22 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -416,7 +416,7 @@ public func recentSessionsController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/Resources/PhoneCountries.txt b/TelegramUI/Resources/PhoneCountries.txt new file mode 100644 index 0000000000..4f541b8866 --- /dev/null +++ b/TelegramUI/Resources/PhoneCountries.txt @@ -0,0 +1,231 @@ +1876;JM;Jamaica +1869;KN;Saint Kitts & Nevis +1868;TT;Trinidad & Tobago +1784;VC;Saint Vincent & the Grenadines +1767;DM;Dominica +1758;LC;Saint Lucia +1721;SX;Sint Maarten +1684;AS;American Samoa +1671;GU;Guam +1670;MP;Northern Mariana Islands +1664;MS;Montserrat +1649;TC;Turks & Caicos Islands +1473;GD;Grenada +1441;BM;Bermuda +1345;KY;Cayman Islands +1340;VI;US Virgin Islands +1284;VG;British Virgin Islands +1268;AG;Antigua & Barbuda +1264;AI;Anguilla +1246;BB;Barbados +1242;BS;Bahamas +998;UZ;Uzbekistan +996;KG;Kyrgyzstan +995;GE;Georgia +994;AZ;Azerbaijan +993;TM;Turkmenistan +992;TJ;Tajikistan +977;NP;Nepal +976;MN;Mongolia +975;BT;Bhutan +974;QA;Qatar +973;BH;Bahrain +972;IL;Israel +971;AE;United Arab Emirates +970;PS;Palestine +968;OM;Oman +967;YE;Yemen +966;SA;Saudi Arabia +965;KW;Kuwait +964;IQ;Iraq +963;SY;Syrian Arab Republic +962;JO;Jordan +961;LB;Lebanon +960;MV;Maldives +886;TW;Taiwan +880;BD;Bangladesh +856;LA;Laos +855;KH;Cambodia +853;MO;Macau +852;HK;Hong Kong +850;KP;North Korea +692;MH;Marshall Islands +691;FM;Micronesia +690;TK;Tokelau +689;PF;French Polynesia +688;TV;Tuvalu +687;NC;New Caledonia +686;KI;Kiribati +685;WS;Samoa +683;NU;Niue +682;CK;Cook Islands +681;WF;Wallis & Futuna +680;PW;Palau +679;FJ;Fiji +678;VU;Vanuatu +677;SB;Solomon Islands +676;TO;Tonga +675;PG;Papua New Guinea +674;NR;Nauru +673;BN;Brunei Darussalam +672;NF;Norfolk Island +670;TL;Timor-Leste +599;BQ;Bonaire, Sint Eustatius & Saba +599;CW;Curaçao +598;UY;Uruguay +597;SR;Suriname +596;MQ;Martinique +595;PY;Paraguay +594;GF;French Guiana +593;EC;Ecuador +592;GY;Guyana +591;BO;Bolivia +590;GP;Guadeloupe +509;HT;Haiti +508;PM;Saint Pierre & Miquelon +507;PA;Panama +506;CR;Costa Rica +505;NI;Nicaragua +504;HN;Honduras +503;SV;El Salvador +502;GT;Guatemala +501;BZ;Belize +500;FK;Falkland Islands +423;LI;Liechtenstein +421;SK;Slovakia +420;CZ;Czech Republic +389;MK;Macedonia +387;BA;Bosnia & Herzegovina +386;SI;Slovenia +385;HR;Croatia +382;ME;Montenegro +381;RS;Serbia +380;UA;Ukraine +378;SM;San Marino +377;MC;Monaco +376;AD;Andorra +375;BY;Belarus +374;AM;Armenia +373;MD;Moldova +372;EE;Estonia +371;LV;Latvia +370;LT;Lithuania +359;BG;Bulgaria +358;FI;Finland +357;CY;Cyprus +356;MT;Malta +355;AL;Albania +354;IS;Iceland +353;IE;Ireland +352;LU;Luxembourg +351;PT;Portugal +350;GI;Gibraltar +299;GL;Greenland +298;FO;Faroe Islands +297;AW;Aruba +291;ER;Eritrea +290;SH;Saint Helena +269;KM;Comoros +268;SZ;Swaziland +267;BW;Botswana +266;LS;Lesotho +265;MW;Malawi +264;NA;Namibia +263;ZW;Zimbabwe +262;RE;Réunion +261;MG;Madagascar +260;ZM;Zambia +258;MZ;Mozambique +257;BI;Burundi +256;UG;Uganda +255;TZ;Tanzania +254;KE;Kenya +253;DJ;Djibouti +252;SO;Somalia +251;ET;Ethiopia +250;RW;Rwanda +249;SD;Sudan +248;SC;Seychelles +247;SH;Saint Helena +246;IO;Diego Garcia +245;GW;Guinea-Bissau +244;AO;Angola +243;CD;Congo (Dem. Rep.) +242;CG;Congo (Rep.) +241;GA;Gabon +240;GQ;Equatorial Guinea +239;ST;São Tomé & Príncipe +238;CV;Cape Verde +237;CM;Cameroon +236;CF;Central African Rep. +235;TD;Chad +234;NG;Nigeria +233;GH;Ghana +232;SL;Sierra Leone +231;LR;Liberia +230;MU;Mauritius +229;BJ;Benin +228;TG;Togo +227;NE;Niger +226;BF;Burkina Faso +225;CI;Côte d`Ivoire +224;GN;Guinea +223;ML;Mali +222;MR;Mauritania +221;SN;Senegal +220;GM;Gambia +218;LY;Libya +216;TN;Tunisia +213;DZ;Algeria +212;MA;Morocco +211;SS;South Sudan +98;IR;Iran +95;MM;Myanmar +94;LK;Sri Lanka +93;AF;Afghanistan +92;PK;Pakistan +91;IN;India +90;TR;Turkey +86;CN;China +84;VN;Vietnam +82;KR;South Korea +81;JP;Japan +66;TH;Thailand +65;SG;Singapore +64;NZ;New Zealand +63;PH;Philippines +62;ID;Indonesia +61;AU;Australia +60;MY;Malaysia +58;VE;Venezuela +57;CO;Colombia +56;CL;Chile +55;BR;Brazil +54;AR;Argentina +53;CU;Cuba +52;MX;Mexico +51;PE;Peru +49;DE;Germany +48;PL;Poland +47;NO;Norway +46;SE;Sweden +45;DK;Denmark +44;GB;United Kingdom +43;AT;Austria +41;CH;Switzerland +40;RO;Romania +39;IT;Italy +36;HU;Hungary +34;ES;Spain +33;FR;France +32;BE;Belgium +31;NL;Netherlands +30;GR;Greece +27;ZA;South Africa +20;EG;Egypt +7;RU;Russian Federation +7;KZ;Kazakhstan +1;US;USA +1;PR;Puerto Rico +1;DO;Dominican Rep. +1;CA;Canada \ No newline at end of file diff --git a/TelegramUI/Resources/Stripe/stp_card_amex@2x.png b/TelegramUI/Resources/Stripe/stp_card_amex@2x.png new file mode 100755 index 0000000000..04feb23eeb Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_amex@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_amex@3x.png b/TelegramUI/Resources/Stripe/stp_card_amex@3x.png new file mode 100755 index 0000000000..a2a34960d2 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_amex@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_amex_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_amex_template@2x.png new file mode 100755 index 0000000000..64eb765a5b Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_amex_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_amex_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_amex_template@3x.png new file mode 100755 index 0000000000..b07fcf2e4f Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_amex_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_applepay@2x.png b/TelegramUI/Resources/Stripe/stp_card_applepay@2x.png new file mode 100755 index 0000000000..4ff86964c7 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_applepay@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_applepay@3x.png b/TelegramUI/Resources/Stripe/stp_card_applepay@3x.png new file mode 100755 index 0000000000..35e7caffa1 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_applepay@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_applepay_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_applepay_template@2x.png new file mode 100755 index 0000000000..fc1b0efea6 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_applepay_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_applepay_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_applepay_template@3x.png new file mode 100755 index 0000000000..950928f5a4 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_applepay_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_cvc@2x.png b/TelegramUI/Resources/Stripe/stp_card_cvc@2x.png new file mode 100755 index 0000000000..7f47afe7d7 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_cvc@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_cvc@3x.png b/TelegramUI/Resources/Stripe/stp_card_cvc@3x.png new file mode 100755 index 0000000000..a0bb31dea9 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_cvc@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_cvc_amex@2x.png b/TelegramUI/Resources/Stripe/stp_card_cvc_amex@2x.png new file mode 100755 index 0000000000..68bee40cce Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_cvc_amex@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_cvc_amex@3x.png b/TelegramUI/Resources/Stripe/stp_card_cvc_amex@3x.png new file mode 100755 index 0000000000..961108d8f4 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_cvc_amex@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_diners@2x.png b/TelegramUI/Resources/Stripe/stp_card_diners@2x.png new file mode 100755 index 0000000000..6733bcc687 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_diners@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_diners@3x.png b/TelegramUI/Resources/Stripe/stp_card_diners@3x.png new file mode 100755 index 0000000000..c24a15233e Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_diners@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_diners_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_diners_template@2x.png new file mode 100755 index 0000000000..9dd1c08d7f Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_diners_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_diners_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_diners_template@3x.png new file mode 100755 index 0000000000..99725e154e Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_diners_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_discover@2x.png b/TelegramUI/Resources/Stripe/stp_card_discover@2x.png new file mode 100755 index 0000000000..1fd9d2a1f8 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_discover@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_discover@3x.png b/TelegramUI/Resources/Stripe/stp_card_discover@3x.png new file mode 100755 index 0000000000..b32cc8fa23 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_discover@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_discover_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_discover_template@2x.png new file mode 100755 index 0000000000..2861f32022 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_discover_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_discover_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_discover_template@3x.png new file mode 100755 index 0000000000..c0770f4f57 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_discover_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_applepay@2x.png b/TelegramUI/Resources/Stripe/stp_card_form_applepay@2x.png new file mode 100755 index 0000000000..705d93bc7e Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_applepay@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_applepay@3x.png b/TelegramUI/Resources/Stripe/stp_card_form_applepay@3x.png new file mode 100755 index 0000000000..23716436e3 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_applepay@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_back@2x.png b/TelegramUI/Resources/Stripe/stp_card_form_back@2x.png new file mode 100755 index 0000000000..cf9adcaaad Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_back@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_back@3x.png b/TelegramUI/Resources/Stripe/stp_card_form_back@3x.png new file mode 100755 index 0000000000..9aa39e3c2f Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_back@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_front@2x.png b/TelegramUI/Resources/Stripe/stp_card_form_front@2x.png new file mode 100755 index 0000000000..69af8e63c1 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_front@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_form_front@3x.png b/TelegramUI/Resources/Stripe/stp_card_form_front@3x.png new file mode 100755 index 0000000000..958022e15e Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_form_front@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_jcb@2x.png b/TelegramUI/Resources/Stripe/stp_card_jcb@2x.png new file mode 100755 index 0000000000..7f0a74b771 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_jcb@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_jcb@3x.png b/TelegramUI/Resources/Stripe/stp_card_jcb@3x.png new file mode 100755 index 0000000000..dab7a5fbdf Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_jcb@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_jcb_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_jcb_template@2x.png new file mode 100755 index 0000000000..5504c9733f Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_jcb_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_jcb_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_jcb_template@3x.png new file mode 100755 index 0000000000..92f1b706a4 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_jcb_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_mastercard@2x.png b/TelegramUI/Resources/Stripe/stp_card_mastercard@2x.png new file mode 100755 index 0000000000..137c3d7c12 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_mastercard@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_mastercard@3x.png b/TelegramUI/Resources/Stripe/stp_card_mastercard@3x.png new file mode 100755 index 0000000000..4a699ba532 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_mastercard@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_mastercard_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_mastercard_template@2x.png new file mode 100755 index 0000000000..ac4b140af4 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_mastercard_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_mastercard_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_mastercard_template@3x.png new file mode 100755 index 0000000000..b879eea2bf Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_mastercard_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_placeholder_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_placeholder_template@2x.png new file mode 100755 index 0000000000..2b55b1f521 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_placeholder_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_placeholder_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_placeholder_template@3x.png new file mode 100755 index 0000000000..c84d9299d8 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_placeholder_template@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_visa@2x.png b/TelegramUI/Resources/Stripe/stp_card_visa@2x.png new file mode 100755 index 0000000000..a5c3eeb02f Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_visa@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_visa@3x.png b/TelegramUI/Resources/Stripe/stp_card_visa@3x.png new file mode 100755 index 0000000000..6b75ed884b Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_visa@3x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_visa_template@2x.png b/TelegramUI/Resources/Stripe/stp_card_visa_template@2x.png new file mode 100755 index 0000000000..12ef2ce182 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_visa_template@2x.png differ diff --git a/TelegramUI/Resources/Stripe/stp_card_visa_template@3x.png b/TelegramUI/Resources/Stripe/stp_card_visa_template@3x.png new file mode 100755 index 0000000000..e95c9a5e34 Binary files /dev/null and b/TelegramUI/Resources/Stripe/stp_card_visa_template@3x.png differ diff --git a/TelegramUI/Resources/currencies.json b/TelegramUI/Resources/currencies.json new file mode 100644 index 0000000000..40a1ead30a --- /dev/null +++ b/TelegramUI/Resources/currencies.json @@ -0,0 +1,1451 @@ +{ + "AED": { + "code": "AED", + "symbol": "د.إ.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "AFN": { + "code": "AFN", + "symbol": "؋", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "ALL": { + "code": "ALL", + "symbol": "Lek", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "AMD": { + "code": "AMD", + "symbol": "֏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "ANG": { + "code": "ANG", + "symbol": "ƒ", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "AOA": { + "code": "AOA", + "symbol": "Kz", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "ARS": { + "code": "ARS", + "symbol": "$", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "AUD": { + "code": "AUD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "AWG": { + "code": "AWG", + "symbol": "ƒ", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "AZN": { + "code": "AZN", + "symbol": "₼", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BAM": { + "code": "BAM", + "symbol": "КМ", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BBD": { + "code": "BBD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "BDT": { + "code": "BDT", + "symbol": "৳", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 0 + }, + "BGN": { + "code": "BGN", + "symbol": "лв.", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BHD": { + "code": "BHD", + "symbol": "د.ب.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 3 + }, + "BIF": { + "code": "BIF", + "symbol": "FBu", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "BMD": { + "code": "BMD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "BND": { + "code": "BND", + "symbol": "$", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "BOB": { + "code": "BOB", + "symbol": "Bs", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BRL": { + "code": "BRL", + "symbol": "R$", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BSD": { + "code": "BSD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "BTC": { + "code": "BTC", + "symbol": "Ƀ", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "BTN": { + "code": "BTN", + "symbol": "Nu.", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 1 + }, + "BWP": { + "code": "BWP", + "symbol": "P", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "BYR": { + "code": "BYR", + "symbol": "р.", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "BZD": { + "code": "BZD", + "symbol": "BZ$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CAD": { + "code": "CAD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CDF": { + "code": "CDF", + "symbol": "FC", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CHF": { + "code": "CHF", + "symbol": "CHF", + "thousandsSeparator": "'", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "CLP": { + "code": "CLP", + "symbol": "$", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "CNY": { + "code": "CNY", + "symbol": "¥", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "COP": { + "code": "COP", + "symbol": "$", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "CRC": { + "code": "CRC", + "symbol": "₡", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CUC": { + "code": "CUC", + "symbol": "CUC", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CUP": { + "code": "CUP", + "symbol": "$MN", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CVE": { + "code": "CVE", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "CZK": { + "code": "CZK", + "symbol": "Kč", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "DJF": { + "code": "DJF", + "symbol": "Fdj", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "DKK": { + "code": "DKK", + "symbol": "kr.", + "thousandsSeparator": "", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "DOP": { + "code": "DOP", + "symbol": "RD$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "DZD": { + "code": "DZD", + "symbol": "د.ج.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "EGP": { + "code": "EGP", + "symbol": "ج.م.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "ERN": { + "code": "ERN", + "symbol": "Nfk", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "ETB": { + "code": "ETB", + "symbol": "ETB", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "EUR": { + "code": "EUR", + "symbol": "€", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "FJD": { + "code": "FJD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "FKP": { + "code": "FKP", + "symbol": "£", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GBP": { + "code": "GBP", + "symbol": "£", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GEL": { + "code": "GEL", + "symbol": "Lari", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "GHS": { + "code": "GHS", + "symbol": "₵", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GIP": { + "code": "GIP", + "symbol": "£", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GMD": { + "code": "GMD", + "symbol": "D", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GNF": { + "code": "GNF", + "symbol": "FG", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "GTQ": { + "code": "GTQ", + "symbol": "Q", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "GYD": { + "code": "GYD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "HKD": { + "code": "HKD", + "symbol": "HK$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "HNL": { + "code": "HNL", + "symbol": "L.", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "HRK": { + "code": "HRK", + "symbol": "kn", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "HTG": { + "code": "HTG", + "symbol": "G", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "HUF": { + "code": "HUF", + "symbol": "Ft", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "IDR": { + "code": "IDR", + "symbol": "Rp", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "ILS": { + "code": "ILS", + "symbol": "₪", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "INR": { + "code": "INR", + "symbol": "₹", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "IQD": { + "code": "IQD", + "symbol": "د.ع.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "IRR": { + "code": "IRR", + "symbol": "﷼", + "thousandsSeparator": ",", + "decimalSeparator": "/", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "ISK": { + "code": "ISK", + "symbol": "kr.", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 0 + }, + "JMD": { + "code": "JMD", + "symbol": "J$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "JOD": { + "code": "JOD", + "symbol": "د.ا.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 3 + }, + "JPY": { + "code": "JPY", + "symbol": "¥", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "KES": { + "code": "KES", + "symbol": "S", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "KGS": { + "code": "KGS", + "symbol": "сом", + "thousandsSeparator": " ", + "decimalSeparator": "-", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "KHR": { + "code": "KHR", + "symbol": "៛", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "KMF": { + "code": "KMF", + "symbol": "CF", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "KPW": { + "code": "KPW", + "symbol": "₩", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "KRW": { + "code": "KRW", + "symbol": "₩", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "KWD": { + "code": "KWD", + "symbol": "د.ك.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 3 + }, + "KYD": { + "code": "KYD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "KZT": { + "code": "KZT", + "symbol": "₸", + "thousandsSeparator": " ", + "decimalSeparator": "-", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "LAK": { + "code": "LAK", + "symbol": "₭", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "LBP": { + "code": "LBP", + "symbol": "ل.ل.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "LKR": { + "code": "LKR", + "symbol": "₨", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 0 + }, + "LRD": { + "code": "LRD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "LSL": { + "code": "LSL", + "symbol": "M", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "LYD": { + "code": "LYD", + "symbol": "د.ل.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 3 + }, + "MAD": { + "code": "MAD", + "symbol": "د.م.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "MDL": { + "code": "MDL", + "symbol": "lei", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "MGA": { + "code": "MGA", + "symbol": "Ar", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "MKD": { + "code": "MKD", + "symbol": "ден.", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "MMK": { + "code": "MMK", + "symbol": "K", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MNT": { + "code": "MNT", + "symbol": "₮", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MOP": { + "code": "MOP", + "symbol": "MOP$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MRO": { + "code": "MRO", + "symbol": "UM", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MTL": { + "code": "MTL", + "symbol": "₤", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MUR": { + "code": "MUR", + "symbol": "₨", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MVR": { + "code": "MVR", + "symbol": "MVR", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 1 + }, + "MWK": { + "code": "MWK", + "symbol": "MK", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MXN": { + "code": "MXN", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MYR": { + "code": "MYR", + "symbol": "RM", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "MZN": { + "code": "MZN", + "symbol": "MT", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "NAD": { + "code": "NAD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "NGN": { + "code": "NGN", + "symbol": "₦", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "NIO": { + "code": "NIO", + "symbol": "C$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "NOK": { + "code": "NOK", + "symbol": "kr", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "NPR": { + "code": "NPR", + "symbol": "₨", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "NZD": { + "code": "NZD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "OMR": { + "code": "OMR", + "symbol": "﷼", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 3 + }, + "PAB": { + "code": "PAB", + "symbol": "B/.", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "PEN": { + "code": "PEN", + "symbol": "S/.", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "PGK": { + "code": "PGK", + "symbol": "K", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "PHP": { + "code": "PHP", + "symbol": "₱", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "PKR": { + "code": "PKR", + "symbol": "₨", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "PLN": { + "code": "PLN", + "symbol": "zł", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "PYG": { + "code": "PYG", + "symbol": "₲", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "QAR": { + "code": "QAR", + "symbol": "﷼", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "RON": { + "code": "RON", + "symbol": "lei", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "RSD": { + "code": "RSD", + "symbol": "Дин.", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "RUB": { + "code": "RUB", + "symbol": "₽", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "RWF": { + "code": "RWF", + "symbol": "RWF", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "SAR": { + "code": "SAR", + "symbol": "﷼", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "SBD": { + "code": "SBD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SCR": { + "code": "SCR", + "symbol": "₨", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SDD": { + "code": "SDD", + "symbol": "LSd", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SDG": { + "code": "SDG", + "symbol": "£‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SEK": { + "code": "SEK", + "symbol": "kr", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "SGD": { + "code": "SGD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SHP": { + "code": "SHP", + "symbol": "£", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SLL": { + "code": "SLL", + "symbol": "Le", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SOS": { + "code": "SOS", + "symbol": "S", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SRD": { + "code": "SRD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "STD": { + "code": "STD", + "symbol": "Db", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SVC": { + "code": "SVC", + "symbol": "₡", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "SYP": { + "code": "SYP", + "symbol": "£", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "SZL": { + "code": "SZL", + "symbol": "E", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "THB": { + "code": "THB", + "symbol": "฿", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "TJS": { + "code": "TJS", + "symbol": "TJS", + "thousandsSeparator": " ", + "decimalSeparator": ";", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "TMT": { + "code": "TMT", + "symbol": "m", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "TND": { + "code": "TND", + "symbol": "د.ت.‏", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 3 + }, + "TOP": { + "code": "TOP", + "symbol": "T$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "TRY": { + "code": "TRY", + "symbol": "TL", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "TTD": { + "code": "TTD", + "symbol": "TT$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "TVD": { + "code": "TVD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "TWD": { + "code": "TWD", + "symbol": "NT$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "TZS": { + "code": "TZS", + "symbol": "TSh", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "UAH": { + "code": "UAH", + "symbol": "₴", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "UGX": { + "code": "UGX", + "symbol": "USh", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "USD": { + "code": "USD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "UYU": { + "code": "UYU", + "symbol": "$U", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "UZS": { + "code": "UZS", + "symbol": "сўм", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "VEB": { + "code": "VEB", + "symbol": "Bs.", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "VEF": { + "code": "VEF", + "symbol": "Bs. F.", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "VND": { + "code": "VND", + "symbol": "₫", + "thousandsSeparator": ".", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 1 + }, + "VUV": { + "code": "VUV", + "symbol": "VT", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 0 + }, + "WST": { + "code": "WST", + "symbol": "WS$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "XAF": { + "code": "XAF", + "symbol": "F", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "XCD": { + "code": "XCD", + "symbol": "$", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "XOF": { + "code": "XOF", + "symbol": "F", + "thousandsSeparator": " ", + "decimalSeparator": ",", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "XPF": { + "code": "XPF", + "symbol": "F", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": false, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "YER": { + "code": "YER", + "symbol": "﷼", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "ZAR": { + "code": "ZAR", + "symbol": "R", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": true, + "decimalDigits": 2 + }, + "ZMW": { + "code": "ZMW", + "symbol": "ZK", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + }, + "WON": { + "code": "WON", + "symbol": "₩", + "thousandsSeparator": ",", + "decimalSeparator": ".", + "symbolOnLeft": true, + "spaceBetweenAmountAndSymbol": false, + "decimalDigits": 2 + } +} \ No newline at end of file diff --git a/TelegramUI/STPAPIClient+ApplePay.h b/TelegramUI/STPAPIClient+ApplePay.h new file mode 100755 index 0000000000..8485269120 --- /dev/null +++ b/TelegramUI/STPAPIClient+ApplePay.h @@ -0,0 +1,32 @@ +// +// STPAPIClient+ApplePay.h +// Stripe +// +// Created by Jack Flintermann on 12/19/14. +// + +#import +#import + +#import "STPAPIClient.h" + +#define FAUXPAS_IGNORED_IN_FILE(...) +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +/** + * STPAPIClient extensions to create Stripe tokens from Apple Pay PKPayment objects. + */ +@interface STPAPIClient (ApplePay) + +/** + * Converts a PKPayment object into a Stripe token using the Stripe API. + * + * @param payment The user's encrypted payment information as returned from a PKPaymentAuthorizationViewController. Cannot be nil. + * @param completion The callback to run with the returned Stripe token (and any errors that may have occurred). + */ +- (void)createTokenWithPayment:(nonnull PKPayment *)payment + completion:(nonnull STPTokenCompletionBlock)completion NS_AVAILABLE_IOS(8_0); + +@end + +void linkSTPAPIClientApplePayCategory(void); diff --git a/TelegramUI/STPAPIClient+ApplePay.m b/TelegramUI/STPAPIClient+ApplePay.m new file mode 100755 index 0000000000..3b683a235e --- /dev/null +++ b/TelegramUI/STPAPIClient+ApplePay.m @@ -0,0 +1,104 @@ +// +// STPAPIClient+ApplePay.m +// Stripe +// +// Created by Jack Flintermann on 12/19/14. +// + +#import + +#import "STPAPIClient+ApplePay.h" +#import "PKPayment+Stripe.h" +#import "STPAPIClient+Private.h" + +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +@implementation STPAPIClient (ApplePay) + +- (void)createTokenWithPayment:(PKPayment *)payment completion:(STPTokenCompletionBlock)completion { + [self createTokenWithData:[self.class formEncodedDataForPayment:payment] + completion:completion]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" ++ (NSData *)formEncodedDataForPayment:(PKPayment *)payment { + NSCAssert(payment != nil, @"Cannot create a token with a nil payment."); + NSMutableCharacterSet *set = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [set removeCharactersInString:@"+="]; + NSString *paymentString = + [[[NSString alloc] initWithData:payment.token.paymentData encoding:NSUTF8StringEncoding] stringByAddingPercentEncodingWithAllowedCharacters:set]; + __block NSString *payloadString = [@"pk_token=" stringByAppendingString:paymentString]; + + ABRecordRef billingAddress = payment.billingAddress; + if (billingAddress) { + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + + NSString *firstName = (__bridge_transfer NSString*)ABRecordCopyValue(billingAddress, kABPersonFirstNameProperty); + NSString *lastName = (__bridge_transfer NSString*)ABRecordCopyValue(billingAddress, kABPersonLastNameProperty); + if (firstName.length && lastName.length) { + params[@"name"] = [NSString stringWithFormat:@"%@ %@", firstName, lastName]; + } + + ABMultiValueRef addressValues = ABRecordCopyValue(billingAddress, kABPersonAddressProperty); + if (addressValues != NULL) { + if (ABMultiValueGetCount(addressValues) > 0) { + CFDictionaryRef dict = ABMultiValueCopyValueAtIndex(addressValues, 0); + NSString *line1 = CFDictionaryGetValue(dict, kABPersonAddressStreetKey); + if (line1) { + params[@"address_line1"] = line1; + } + NSString *city = CFDictionaryGetValue(dict, kABPersonAddressCityKey); + if (city) { + params[@"address_city"] = city; + } + NSString *state = CFDictionaryGetValue(dict, kABPersonAddressStateKey); + if (state) { + params[@"address_state"] = state; + } + NSString *zip = CFDictionaryGetValue(dict, kABPersonAddressZIPKey); + if (zip) { + params[@"address_zip"] = zip; + } + NSString *country = CFDictionaryGetValue(dict, kABPersonAddressCountryKey); + if (country) { + params[@"address_country"] = country; + } + CFRelease(dict); + [params enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, __unused BOOL *stop) { + NSString *param = [NSString stringWithFormat:@"&card[%@]=%@", key, [obj stringByAddingPercentEncodingWithAllowedCharacters:set]]; + payloadString = [payloadString stringByAppendingString:param]; + }]; + } + CFRelease(addressValues); + } + } + + NSString *paymentInstrumentName = payment.token.paymentInstrumentName; + if (paymentInstrumentName) { + NSString *param = [NSString stringWithFormat:@"&pk_token_instrument_name=%@", paymentInstrumentName]; + payloadString = [payloadString stringByAppendingString:param]; + } + + NSString *paymentNetwork = payment.token.paymentNetwork; + if (paymentNetwork) { + NSString *param = [NSString stringWithFormat:@"&pk_token_payment_network=%@", paymentNetwork]; + payloadString = [payloadString stringByAppendingString:param]; + } + + NSString *transactionIdentifier = payment.token.transactionIdentifier; + if (transactionIdentifier) { + if ([payment stp_isSimulated]) { + transactionIdentifier = [PKPayment stp_testTransactionIdentifier]; + } + NSString *param = [NSString stringWithFormat:@"&pk_token_transaction_id=%@", transactionIdentifier]; + payloadString = [payloadString stringByAppendingString:param]; + } + + return [payloadString dataUsingEncoding:NSUTF8StringEncoding]; +} +#pragma clang diagnostic pop + +@end + +void linkSTPAPIClientApplePayCategory(void){} diff --git a/TelegramUI/STPAPIClient+Private.h b/TelegramUI/STPAPIClient+Private.h new file mode 100755 index 0000000000..4073f6e8e5 --- /dev/null +++ b/TelegramUI/STPAPIClient+Private.h @@ -0,0 +1,26 @@ +// +// STPAPIClient+Private.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPAPIClient() + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey + baseURL:(NSString *)baseURL; + +- (void)createTokenWithData:(NSData *)data + completion:(STPTokenCompletionBlock)completion; + +@property (nonatomic, readwrite) NSURL *apiURL; +@property (nonatomic, readwrite) NSURLSession *urlSession; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPAPIClient.h b/TelegramUI/STPAPIClient.h new file mode 100755 index 0000000000..c7113f7852 --- /dev/null +++ b/TelegramUI/STPAPIClient.h @@ -0,0 +1,208 @@ +// +// STPAPIClient.h +// StripeExample +// +// Created by Jack Flintermann on 12/18/14. +// Copyright (c) 2014 Stripe. All rights reserved. +// + +#import +#import +#import "STPBlocks.h" +#import "STPCardBrand.h" + +NS_ASSUME_NONNULL_BEGIN + +#define FAUXPAS_IGNORED_ON_LINE(...) +#define FAUXPAS_IGNORED_IN_FILE(...) +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +static NSString *const STPSDKVersion = @"9.1.0"; + +@class STPBankAccount, STPBankAccountParams, STPCard, STPCardParams, STPToken, STPPaymentConfiguration; + +/** + A top-level class that imports the rest of the Stripe SDK. This class used to contain several methods to create Stripe tokens, but those are now deprecated in + favor of STPAPIClient. + */ +@interface Stripe : NSObject FAUXPAS_IGNORED_ON_LINE(UnprefixedClass); + +/** + * Set your Stripe API key with this method. New instances of STPAPIClient will be initialized with this value. You should call this method as early as + * possible in your application's lifecycle, preferably in your AppDelegate. + * + * @param publishableKey Your publishable key, obtained from https://stripe.com/account/apikeys + * @warning Make sure not to ship your test API keys to the App Store! This will log a warning if you use your test key in a release build. + */ ++ (void)setDefaultPublishableKey:(NSString *)publishableKey; + +/// The current default publishable key. ++ (nullable NSString *)defaultPublishableKey; + +/** + * By default, Stripe collects some basic information about SDK usage. + * You can call this method to turn off analytics collection. + */ ++ (void)disableAnalytics; + +@end + +/// A client for making connections to the Stripe API. +@interface STPAPIClient : NSObject + ++ (NSString *)stringWithCardBrand:(STPCardBrand)brand; + +/** + * A shared singleton API client. Its API key will be initially equal to [Stripe defaultPublishableKey]. + */ ++ (instancetype)sharedClient; +- (instancetype)initWithConfiguration:(STPPaymentConfiguration *)configuration NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithPublishableKey:(NSString *)publishableKey; + +/** + * @see [Stripe setDefaultPublishableKey:] + */ +@property (nonatomic, copy, nullable) NSString *publishableKey; + +/** + * @see -initWithConfiguration + */ +@property (nonatomic, copy) STPPaymentConfiguration *configuration; + +- (void)createTokenWithCard:(STPCardParams *)card completion:(nullable STPTokenCompletionBlock)completion; + +@end + +#pragma mark Bank Accounts + +/** + * STPAPIClient extensions to create Stripe tokens from bank accounts. + */ +@interface STPAPIClient (BankAccounts) + +/** + * Converts an STPBankAccount object into a Stripe token using the Stripe API. + * + * @param bankAccount The user's bank account details. Cannot be nil. @see https://stripe.com/docs/api#create_bank_account_token + * @param completion The callback to run with the returned Stripe token (and any errors that may have occurred). + */ +- (void)createTokenWithBankAccount:(STPBankAccountParams *)bankAccount completion:(__nullable STPTokenCompletionBlock)completion; + +@end + +#pragma mark Credit Cards + +/** + * STPAPIClient extensions to create Stripe tokens from credit or debit cards. + */ +@interface STPAPIClient (CreditCards) + +/** + * Converts an STPCardParams object into a Stripe token using the Stripe API. + * + * @param card The user's card details. Cannot be nil. @see https://stripe.com/docs/api#create_card_token + * @param completion The callback to run with the returned Stripe token (and any errors that may have occurred). + */ + + +@end + +/** + * Convenience methods for working with Apple Pay. + */ +@interface Stripe(ApplePay) + +/** + * Whether or not this device is capable of using Apple Pay. This checks both whether the user is running an iPhone 6/6+ or later, iPad Air 2 or later, or iPad + *mini 3 or later, as well as whether or not they have stored any cards in Apple Pay on their device. + * + * @param paymentRequest The return value of this method depends on the `supportedNetworks` property of this payment request, which by default should be + *`@[PKPaymentNetworkAmex, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa, PKPaymentNetworkDiscover]`. + * + * @return whether or not the user is currently able to pay with Apple Pay. + */ ++ (BOOL)canSubmitPaymentRequest:(PKPaymentRequest *)paymentRequest NS_AVAILABLE_IOS(8_0); + ++ (BOOL)deviceSupportsApplePay; + +/** + * A convenience method to return a `PKPaymentRequest` with sane default values. You will still need to configure the `paymentSummaryItems` property to indicate + *what the user is purchasing, as well as the optional `requiredShippingAddressFields`, `requiredBillingAddressFields`, and `shippingMethods` properties to indicate + *what contact information your application requires. + * + * @param merchantIdentifier Your Apple Merchant ID, as obtained at https://developer.apple.com/account/ios/identifiers/merchant/merchantCreate.action + * + * @return a `PKPaymentRequest` with proper default values. Returns nil if running on < iOS8. + */ ++ (PKPaymentRequest *)paymentRequestWithMerchantIdentifier:(NSString *)merchantIdentifier NS_AVAILABLE_IOS(8_0); + ++ (void)createTokenWithPayment:(PKPayment *)payment + completion:(STPTokenCompletionBlock)handler __attribute__((deprecated("Use STPAPIClient instead."))); + +@end + +#pragma mark - Deprecated Methods + +/** + * A callback to be run with a token response from the Stripe API. + * + * @param token The Stripe token from the response. Will be nil if an error occurs. @see STPToken + * @param error The error returned from the response, or nil in one occurs. @see StripeError.h for possible values. + * @deprecated This has been renamed to STPTokenCompletionBlock. + */ +typedef void (^STPCompletionBlock)(STPToken * __nullable token, NSError * __nullable error) __attribute__((deprecated("STPCompletionBlock has been renamed to STPTokenCompletionBlock."))); + +// These methods are deprecated. You should instead use STPAPIClient to create tokens. +// Example: [Stripe createTokenWithCard:card completion:completion]; +// becomes [[STPAPIClient sharedClient] createTokenWithCard:card completion:completion]; +@interface Stripe (Deprecated) + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param card The user's card details. @see STPCard + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithCard:(STPCard *)card completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. + * + * @param card The user's card details. @see STPCard + * @param publishableKey The API key to use to authenticate with Stripe. Get this at https://stripe.com/account/apikeys . + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithCard:(STPCard *)card publishableKey:(NSString *)publishableKey completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param bankAccount The user's bank account details. @see STPBankAccount + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param bankAccount The user's bank account details. @see STPBankAccount + * @param publishableKey The API key to use to authenticate with Stripe. Get this at https://stripe.com/account/apikeys . + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount + publishableKey:(NSString *)publishableKey + completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + ++ (BOOL)deviceSupportsApplePay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPAPIClient.m b/TelegramUI/STPAPIClient.m new file mode 100755 index 0000000000..bfe2ef58c9 --- /dev/null +++ b/TelegramUI/STPAPIClient.m @@ -0,0 +1,318 @@ +// +// STPAPIClient.m +// StripeExample +// +// Created by Jack Flintermann on 12/18/14. +// Copyright (c) 2014 Stripe. All rights reserved. +// + +#import +#import + +#import "STPAPIClient.h" +#import "STPAPIClient+ApplePay.h" +#import "STPFormEncoder.h" +#import "STPBankAccount.h" +#import "STPCard.h" +#import "STPToken.h" +#import "STPAPIPostRequest.h" +#import "STPPaymentConfiguration.h" +#import "NSString+Stripe_CardBrands.h" + +#if __has_include("Fabric.h") +#import "Fabric+FABKits.h" +#import "FABKitProtocol.h" +#endif + +#ifdef STP_STATIC_LIBRARY_BUILD +#import "STPCategoryLoader.h" +#endif + +#define FAUXPAS_IGNORED_IN_METHOD(...) +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +static NSString *const apiURLBase = @"api.stripe.com/v1"; +static NSString *const tokenEndpoint = @"tokens"; +static NSString *const stripeAPIVersion = @"2015-10-12"; + +@implementation Stripe + ++ (void)setDefaultPublishableKey:(NSString *)publishableKey { + [STPPaymentConfiguration sharedConfiguration].publishableKey = publishableKey; +} + ++ (NSString *)defaultPublishableKey { + return [STPPaymentConfiguration sharedConfiguration].publishableKey; +} + ++ (void)disableAnalytics { +} + +@end + +#if __has_include("Fabric.h") +@interface STPAPIClient () +#else +@interface STPAPIClient() +#endif +@property (nonatomic, readwrite) NSURL *apiURL; +@property (nonatomic, readwrite) NSURLSession *urlSession; +@end + +@implementation STPAPIClient + ++ (NSString *)stringWithCardBrand:(STPCardBrand)brand { + return [NSString stp_stringWithCardBrand:brand]; +} + ++ (void)initialize { +#ifdef STP_STATIC_LIBRARY_BUILD + [STPCategoryLoader loadCategories]; +#endif +} + ++ (instancetype)sharedClient { + static id sharedClient; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ sharedClient = [[self alloc] init]; }); + return sharedClient; +} + +- (instancetype)init { + return [self initWithConfiguration:[STPPaymentConfiguration sharedConfiguration]]; +} + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey { + STPPaymentConfiguration *config = [[STPPaymentConfiguration alloc] init]; + config.publishableKey = [publishableKey copy]; + [self.class validateKey:publishableKey]; + return [self initWithConfiguration:config]; +} + +- (instancetype)initWithConfiguration:(STPPaymentConfiguration *)configuration { + self = [super init]; + if (self) { + _apiURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", apiURLBase]]; + _configuration = configuration; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSString *auth = [@"Bearer " stringByAppendingString:self.publishableKey]; + sessionConfiguration.HTTPAdditionalHeaders = @{ + @"X-Stripe-User-Agent": [self.class stripeUserAgentDetails], + @"Stripe-Version": stripeAPIVersion, + @"Authorization": auth, + }; + _urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + } + return self; +} + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey + baseURL:(NSString *)baseURL { + self = [self initWithPublishableKey:publishableKey]; + if (self) { + _apiURL = [NSURL URLWithString:baseURL]; + } + return self; +} + +- (void)setPublishableKey:(NSString *)publishableKey { + self.configuration.publishableKey = [publishableKey copy]; +} + +- (NSString *)publishableKey { + return self.configuration.publishableKey; +} + +- (void)createTokenWithData:(NSData *)data + completion:(STPTokenCompletionBlock)completion { + NSCAssert(data != nil, @"'data' is required to create a token"); + NSCAssert(completion != nil, @"'completion' is required to use the token that is created"); + [STPAPIPostRequest startWithAPIClient:self + endpoint:tokenEndpoint + postData:data + serializer:[STPToken new] + completion:^(STPToken *object, NSHTTPURLResponse *response, NSError *error) { + completion(object, error); + }]; +} + +- (void)createTokenWithCard:(STPCard *)card completion:(STPTokenCompletionBlock)completion { + NSData *data = [STPFormEncoder formEncodedDataForObject:card]; + [self createTokenWithData:data completion:completion]; +} + +#pragma mark - private helpers + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-variable" ++ (void)validateKey:(NSString *)publishableKey { + NSCAssert(publishableKey != nil && ![publishableKey isEqualToString:@""], + @"You must use a valid publishable key to create a token. For more info, see https://stripe.com/docs/stripe.js"); + BOOL secretKey = [publishableKey hasPrefix:@"sk_"]; + NSCAssert(!secretKey, + @"You are using a secret key to create a token, instead of the publishable one. For more info, see https://stripe.com/docs/stripe.js"); +#ifndef DEBUG + if ([publishableKey.lowercaseString hasPrefix:@"pk_test"]) { + FAUXPAS_IGNORED_IN_METHOD(NSLogUsed); + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSLog(@"ℹ️ You're using your Stripe testmode key. Make sure to use your livemode key when submitting to the App Store!"); + }); + } +#endif +} +#pragma clang diagnostic pop + +#pragma mark Utility methods - + ++ (NSString *)stripeUserAgentDetails { + NSMutableDictionary *details = [@{ + @"lang": @"objective-c", + @"bindings_version": STPSDKVersion, + } mutableCopy]; + NSString *version = [UIDevice currentDevice].systemVersion; + if (version) { + details[@"os_version"] = version; + } + struct utsname systemInfo; + uname(&systemInfo); + NSString *deviceType = @(systemInfo.machine); + if (deviceType) { + details[@"type"] = deviceType; + } + NSString *model = [UIDevice currentDevice].localizedModel; + if (model) { + details[@"model"] = model; + } + if ([[UIDevice currentDevice] respondsToSelector:@selector(identifierForVendor)]) { + NSString *vendorIdentifier = [[[UIDevice currentDevice] performSelector:@selector(identifierForVendor)] performSelector:@selector(UUIDString)]; + if (vendorIdentifier) { + details[@"vendor_identifier"] = vendorIdentifier; + } + } + return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[details copy] options:0 error:NULL] encoding:NSUTF8StringEncoding]; +} + +#pragma mark Fabric +#if __has_include("Fabric.h") + ++ (NSString *)bundleIdentifier { + return @"com.stripe.stripe-ios"; +} + ++ (NSString *)kitDisplayVersion { + return STPSDKVersion; +} + ++ (void)initializeIfNeeded { + Class fabric = NSClassFromString(@"Fabric"); + if (fabric) { + // The app must be using Fabric, as it exists at runtime. We fetch our default publishable key from Fabric. + NSDictionary *fabricConfiguration = [fabric configurationDictionaryForKitClass:[STPAPIClient class]]; + NSString *publishableKey = fabricConfiguration[@"publishable"]; + if (!publishableKey) { + NSLog(@"Configuration dictionary returned by Fabric was nil, or doesn't have publishableKey. Can't initialize Stripe."); + return; + } + [self validateKey:publishableKey]; + [Stripe setDefaultPublishableKey:publishableKey]; + } else { + NSCAssert(fabric, @"initializeIfNeeded method called from a project that doesn't have Fabric."); + } +} + +#endif + +@end + +#pragma mark - Bank Accounts +@implementation STPAPIClient (BankAccounts) + +- (void)createTokenWithBankAccount:(STPBankAccountParams *)bankAccount + completion:(STPTokenCompletionBlock)completion { + NSData *data = [STPFormEncoder formEncodedDataForObject:bankAccount]; + [self createTokenWithData:data completion:completion]; +} + +@end + +#pragma mark - Credit Cards + +@implementation Stripe (ApplePay) + ++ (BOOL)canSubmitPaymentRequest:(PKPaymentRequest *)paymentRequest { + if (![self deviceSupportsApplePay]) { + return NO; + } + if (paymentRequest == nil) { + return NO; + } + if (paymentRequest.merchantIdentifier == nil) { + return NO; + } + return [[[paymentRequest.paymentSummaryItems lastObject] amount] floatValue] > 0; +} + ++ (NSArray *)supportedPKPaymentNetworks { + NSArray *supportedNetworks = @[PKPaymentNetworkAmex, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa]; + if ((&PKPaymentNetworkDiscover) != NULL) { + supportedNetworks = [supportedNetworks arrayByAddingObject:PKPaymentNetworkDiscover]; + } + return supportedNetworks; +} + ++ (BOOL)deviceSupportsApplePay { + return [PKPaymentAuthorizationViewController class] && [PKPaymentAuthorizationViewController canMakePaymentsUsingNetworks:[self supportedPKPaymentNetworks]]; +} + ++ (PKPaymentRequest *)paymentRequestWithMerchantIdentifier:(NSString *)merchantIdentifier { + if (![PKPaymentRequest class]) { + return nil; + } + PKPaymentRequest *paymentRequest = [PKPaymentRequest new]; + [paymentRequest setMerchantIdentifier:merchantIdentifier]; + [paymentRequest setSupportedNetworks:[self supportedPKPaymentNetworks]]; + [paymentRequest setMerchantCapabilities:PKMerchantCapability3DS]; + [paymentRequest setCountryCode:@"US"]; + [paymentRequest setCurrencyCode:@"USD"]; + return paymentRequest; +} + ++ (void)createTokenWithPayment:(PKPayment *)payment + completion:(STPTokenCompletionBlock)handler { + [[STPAPIClient sharedClient] createTokenWithPayment:payment completion:handler]; +} + +@end + +@implementation Stripe (Deprecated) + ++ (id)alloc { + NSCAssert(NO, @"'Stripe' is a static class and cannot be instantiated."); + return nil; +} + +#pragma mark Shorthand methods - + ++ (void)createTokenWithCard:(STPCard *)card completion:(STPCompletionBlock)handler { + [[STPAPIClient sharedClient] createTokenWithCard:card completion:handler]; +} + ++ (void)createTokenWithCard:(STPCard *)card publishableKey:(NSString *)publishableKey completion:(STPCompletionBlock)handler { + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + config.publishableKey = publishableKey; + [[[STPAPIClient alloc] initWithConfiguration:config] createTokenWithCard:card completion:handler]; +} + ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount completion:(STPCompletionBlock)handler { + [[STPAPIClient sharedClient] createTokenWithBankAccount:bankAccount completion:handler]; +} + ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount publishableKey:(NSString *)publishableKey completion:(STPCompletionBlock)handler { + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + config.publishableKey = publishableKey; + [[[STPAPIClient alloc] initWithConfiguration:config] createTokenWithBankAccount:bankAccount completion:handler]; +} + +@end diff --git a/TelegramUI/STPAPIPostRequest.h b/TelegramUI/STPAPIPostRequest.h new file mode 100755 index 0000000000..49ebb210d3 --- /dev/null +++ b/TelegramUI/STPAPIPostRequest.h @@ -0,0 +1,23 @@ +// +// STPAPIPostRequest.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import "STPAPIResponseDecodable.h" +@class STPAPIClient; + +@interface STPAPIPostRequest<__covariant ResponseType:id> : NSObject + +typedef void(^STPAPIPostResponseBlock)(ResponseType object, NSHTTPURLResponse *response, NSError *error); + ++ (void)startWithAPIClient:(STPAPIClient *)apiClient + endpoint:(NSString *)endpoint + postData:(NSData *)postData + serializer:(ResponseType)serializer + completion:(STPAPIPostResponseBlock)completion; + +@end diff --git a/TelegramUI/STPAPIPostRequest.m b/TelegramUI/STPAPIPostRequest.m new file mode 100755 index 0000000000..733d1283a9 --- /dev/null +++ b/TelegramUI/STPAPIPostRequest.m @@ -0,0 +1,51 @@ +// +// STPAPIPostRequest.m +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPAPIPostRequest.h" +#import "STPAPIClient.h" +#import "STPAPIClient+Private.h" +#import "StripeError.h" +#import "STPDispatchFunctions.h" + +@implementation STPAPIPostRequest + ++ (void)startWithAPIClient:(STPAPIClient *)apiClient + endpoint:(NSString *)endpoint + postData:(NSData *)postData + serializer:(id)serializer + completion:(STPAPIPostResponseBlock)completion { + + NSURL *url = [apiClient.apiURL URLByAppendingPathComponent:endpoint]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBody = postData; + + [[apiClient.urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable body, NSURLResponse * _Nullable response, NSError * _Nullable error) { + NSDictionary *jsonDictionary = body ? [NSJSONSerialization JSONObjectWithData:body options:0 error:NULL] : nil; + id responseObject = [[serializer class] decodedObjectFromAPIResponse:jsonDictionary]; + NSError *returnedError = [NSError stp_errorFromStripeResponse:jsonDictionary] ?: error; + if ((!responseObject || ![response isKindOfClass:[NSHTTPURLResponse class]]) && !returnedError) { + returnedError = [NSError stp_genericFailedToParseResponseError]; + } + + NSHTTPURLResponse *httpResponse; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + httpResponse = (NSHTTPURLResponse *)response; + } + stpDispatchToMainThreadIfNecessary(^{ + if (returnedError) { + completion(nil, httpResponse, returnedError); + } else { + completion(responseObject, httpResponse, nil); + } + }); + }] resume]; + +} + +@end diff --git a/TelegramUI/STPAPIResponseDecodable.h b/TelegramUI/STPAPIResponseDecodable.h new file mode 100755 index 0000000000..d90b38517b --- /dev/null +++ b/TelegramUI/STPAPIResponseDecodable.h @@ -0,0 +1,28 @@ +// +// STPAPIResponseDecodable.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@protocol STPAPIResponseDecodable + +/** + * These fields are required to be present in the API response. If any of them are nil, `decodedObjectFromAPIResponse` should also return nil. + */ ++ (nonnull NSArray *)requiredFields; + +/** + * Parses an response from the Stripe API (in JSON format; represented as an `NSDictionary`) into an instance of the class. Returns nil if the object could not be decoded (i.e. if one of its `requiredFields` is nil). + */ ++ (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)response; + +/** + * The raw JSON response used to create the object. This can be useful for using beta features that haven't yet been made into properties in the SDK. + */ +@property(nonatomic, readonly, nonnull, copy)NSDictionary *allResponseFields; + +@end diff --git a/TelegramUI/STPAddress.h b/TelegramUI/STPAddress.h new file mode 100755 index 0000000000..aeb90ee1f8 --- /dev/null +++ b/TelegramUI/STPAddress.h @@ -0,0 +1,99 @@ +// +// STPAddress.h +// Stripe +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" +#import +#pragma clang diagnostic pop + +#define FAUXPAS_IGNORED_IN_METHOD(...) +#define FAUXPAS_IGNORED_ON_LINE(...) + +#import +#import + +/** + * What set of billing address information you need to collect from your user. + * + * @note If the user is from a country that does not use zip/postal codes, + * the user may not be asked for one regardless of this setting. + */ +typedef NS_ENUM(NSUInteger, STPBillingAddressFields) { + /** + * No billing address information + */ + STPBillingAddressFieldsNone, + /** + * Just request the user's billing ZIP code + */ + STPBillingAddressFieldsZip, + /** + * Request the user's full billing address + */ + STPBillingAddressFieldsFull, +}; + +/** + * STPAddress Contains an address as represented by the Stripe API. + */ +@interface STPAddress : NSObject + +/** + * The user's full name (e.g. "Jane Doe") + */ +@property (nonatomic, copy) NSString *name; + +/** + * The first line of the user's street address (e.g. "123 Fake St") + */ +@property (nonatomic, copy) NSString *line1; + +/** + * The apartment, floor number, etc of the user's street address (e.g. "Apartment 1A") + */ +@property (nonatomic, copy) NSString *line2; + +/** + * The city in which the user resides (e.g. "San Francisco") + */ +@property (nonatomic, copy) NSString *city; + +/** + * The state in which the user resides (e.g. "CA") + */ +@property (nonatomic, copy) NSString *state; + +/** + * The postal code in which the user resides (e.g. "90210") + */ +@property (nonatomic, copy) NSString *postalCode; + +/** + * The ISO country code of the address (e.g. "US") + */ +@property (nonatomic, copy) NSString *country; + +/** + * The phone number of the address (e.g. "8885551212") + */ +@property (nonatomic, copy) NSString *phone; + +/** + * The email of the address (e.g. "jane@doe.com") + */ +@property (nonatomic, copy) NSString *email; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" +- (instancetype)initWithABRecord:(ABRecordRef)record; +#pragma clang diagnostic pop + +- (BOOL)containsRequiredFields:(STPBillingAddressFields)requiredFields; + ++ (PKAddressField)applePayAddressFieldsFromBillingAddressFields:(STPBillingAddressFields)billingAddressFields; FAUXPAS_IGNORED_ON_LINE(APIAvailability); + +@end diff --git a/TelegramUI/STPAddress.m b/TelegramUI/STPAddress.m new file mode 100755 index 0000000000..102cd04b72 --- /dev/null +++ b/TelegramUI/STPAddress.m @@ -0,0 +1,106 @@ +// +// STPAddress.m +// Stripe +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPAddress.h" +#import "STPCardValidator.h" +#import "STPPostalCodeValidator.h" + +@implementation STPAddress + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + +- (instancetype)initWithABRecord:(ABRecordRef)record { + self = [super init]; + if (self) { + NSString *firstName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonFirstNameProperty); + NSString *lastName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonLastNameProperty); + NSString *first = firstName ?: @""; + NSString *last = lastName ?: @""; + _name = [@[first, last] componentsJoinedByString:@" "]; + + ABMultiValueRef emailValues = ABRecordCopyValue(record, kABPersonEmailProperty); + _email = (__bridge_transfer NSString *)(ABMultiValueCopyValueAtIndex(emailValues, 0)); + CFRelease(emailValues); + + ABMultiValueRef phoneValues = ABRecordCopyValue(record, kABPersonPhoneProperty); + NSString *phone = (__bridge_transfer NSString *)(ABMultiValueCopyValueAtIndex(phoneValues, 0)); + CFRelease(phoneValues); + + _phone = [STPCardValidator sanitizedNumericStringForString:phone]; + + ABMultiValueRef addressValues = ABRecordCopyValue(record, kABPersonAddressProperty); + if (addressValues != NULL) { + if (ABMultiValueGetCount(addressValues) > 0) { + CFDictionaryRef dict = ABMultiValueCopyValueAtIndex(addressValues, 0); + NSString *street = CFDictionaryGetValue(dict, kABPersonAddressStreetKey); + if (street) { + _line1 = street; + } + NSString *city = CFDictionaryGetValue(dict, kABPersonAddressCityKey); + if (city) { + _city = city; + } + NSString *state = CFDictionaryGetValue(dict, kABPersonAddressStateKey); + if (state) { + _state = state; + } + NSString *zip = CFDictionaryGetValue(dict, kABPersonAddressZIPKey); + if (zip) { + _postalCode = zip; + } + NSString *country = CFDictionaryGetValue(dict, kABPersonAddressCountryCodeKey); + if (country) { + _country = country; + } + CFRelease(dict); + } + CFRelease(addressValues); + } + } + return self; +} + +#pragma clang diagnostic pop + +- (BOOL)containsRequiredFields:(STPBillingAddressFields)requiredFields { + BOOL containsFields = YES; + switch (requiredFields) { + case STPBillingAddressFieldsNone: + return YES; + case STPBillingAddressFieldsZip: + return [STPPostalCodeValidator stringIsValidPostalCode:self.postalCode + countryCode:self.country]; + case STPBillingAddressFieldsFull: + return [self hasValidPostalAddress]; + } + return containsFields; +} + +- (BOOL)hasValidPostalAddress { + return (self.line1.length > 0 + && self.city.length > 0 + && self.country.length > 0 + && (self.state.length > 0 || ![self.country isEqualToString:@"US"]) + && [STPPostalCodeValidator stringIsValidPostalCode:self.postalCode + countryCode:self.country]); +} + ++ (PKAddressField)applePayAddressFieldsFromBillingAddressFields:(STPBillingAddressFields)billingAddressFields { + FAUXPAS_IGNORED_IN_METHOD(APIAvailability); + switch (billingAddressFields) { + case STPBillingAddressFieldsNone: + return PKAddressFieldNone; + case STPBillingAddressFieldsZip: + case STPBillingAddressFieldsFull: + return PKAddressFieldPostalAddress; + } +} + +@end + diff --git a/TelegramUI/STPBINRange.h b/TelegramUI/STPBINRange.h new file mode 100755 index 0000000000..e66d83b289 --- /dev/null +++ b/TelegramUI/STPBINRange.h @@ -0,0 +1,26 @@ +// +// STPBINRange.h +// Stripe +// +// Created by Jack Flintermann on 5/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import "STPCardBrand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface STPBINRange : NSObject + +@property(nonatomic, readonly)NSUInteger length; +@property(nonatomic, readonly)STPCardBrand brand; + ++ (NSArray *)allRanges; ++ (NSArray *)binRangesForNumber:(NSString *)number; ++ (NSArray *)binRangesForBrand:(STPCardBrand)brand; ++ (instancetype)mostSpecificBINRangeForNumber:(NSString *)number; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPBINRange.m b/TelegramUI/STPBINRange.m new file mode 100755 index 0000000000..9998b79178 --- /dev/null +++ b/TelegramUI/STPBINRange.m @@ -0,0 +1,114 @@ +// +// STPBINRange.m +// Stripe +// +// Created by Jack Flintermann on 5/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPBINRange.h" +#import "NSString+Stripe.h" + +@interface STPBINRange() + +@property(nonatomic)NSUInteger length; +@property(nonatomic)NSString *qRangeLow; +@property(nonatomic)NSString *qRangeHigh; +@property(nonatomic)STPCardBrand brand; + +- (BOOL)matchesNumber:(NSString *)number; + +@end + + +@implementation STPBINRange + ++ (NSArray *)allRanges { + + static NSArray *STPBINRangeAllRanges; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSArray *ranges = @[ + // Catch-all values + @[@"", @"", @16, @(STPCardBrandUnknown)], + @[@"34", @"34", @15, @(STPCardBrandAmex)], + @[@"37", @"37", @15, @(STPCardBrandAmex)], + @[@"30", @"30", @14, @(STPCardBrandDinersClub)], + @[@"36", @"36", @14, @(STPCardBrandDinersClub)], + @[@"38", @"39", @14, @(STPCardBrandDinersClub)], + @[@"6011", @"6011", @16, @(STPCardBrandDiscover)], + @[@"622", @"622", @16, @(STPCardBrandDiscover)], + @[@"64", @"65", @16, @(STPCardBrandDiscover)], + @[@"35", @"35", @16, @(STPCardBrandJCB)], + @[@"5", @"5", @16, @(STPCardBrandMasterCard)], + @[@"4", @"4", @16, @(STPCardBrandVisa)], + // Specific known BIN ranges + @[@"222100", @"272099", @16, @(STPCardBrandMasterCard)], + + @[@"413600", @"413600", @13, @(STPCardBrandVisa)], + @[@"444509", @"444509", @13, @(STPCardBrandVisa)], + @[@"444509", @"444509", @13, @(STPCardBrandVisa)], + @[@"444550", @"444550", @13, @(STPCardBrandVisa)], + @[@"450603", @"450603", @13, @(STPCardBrandVisa)], + @[@"450617", @"450617", @13, @(STPCardBrandVisa)], + @[@"450628", @"450629", @13, @(STPCardBrandVisa)], + @[@"450636", @"450636", @13, @(STPCardBrandVisa)], + @[@"450640", @"450641", @13, @(STPCardBrandVisa)], + @[@"450662", @"450662", @13, @(STPCardBrandVisa)], + @[@"463100", @"463100", @13, @(STPCardBrandVisa)], + @[@"476142", @"476142", @13, @(STPCardBrandVisa)], + @[@"476143", @"476143", @13, @(STPCardBrandVisa)], + @[@"492901", @"492902", @13, @(STPCardBrandVisa)], + @[@"492920", @"492920", @13, @(STPCardBrandVisa)], + @[@"492923", @"492923", @13, @(STPCardBrandVisa)], + @[@"492928", @"492930", @13, @(STPCardBrandVisa)], + @[@"492937", @"492937", @13, @(STPCardBrandVisa)], + @[@"492939", @"492939", @13, @(STPCardBrandVisa)], + @[@"492960", @"492960", @13, @(STPCardBrandVisa)], + ]; + NSMutableArray *binRanges = [NSMutableArray array]; + for (NSArray *range in ranges) { + STPBINRange *binRange = [self.class new]; + binRange.qRangeLow = range[0]; + binRange.qRangeHigh = range[1]; + binRange.length = [range[2] unsignedIntegerValue]; + binRange.brand = [range[3] integerValue]; + [binRanges addObject:binRange]; + } + STPBINRangeAllRanges = [binRanges copy]; + }); + return STPBINRangeAllRanges; +} + +- (BOOL)matchesNumber:(NSString *)number { + NSString *low = [number stringByPaddingToLength:self.qRangeLow.length withString:@"0" startingAtIndex:0]; + NSString *high = [number stringByPaddingToLength:self.qRangeHigh.length withString:@"0" startingAtIndex:0]; + + return self.qRangeLow.integerValue <= low.integerValue && self.qRangeHigh.integerValue >= high.integerValue; +} + +- (NSComparisonResult)compare:(STPBINRange *)other { + return [@(self.qRangeLow.length) compare:@(other.qRangeLow.length)]; +} + ++ (NSArray *)binRangesForNumber:(NSString *)number { + return [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return [range matchesNumber:number]; + }]]; +} + ++ (instancetype)mostSpecificBINRangeForNumber:(NSString *)number { + NSArray *validRanges = [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return [range matchesNumber:number]; + }]]; + return [[validRanges sortedArrayUsingSelector:@selector(compare:)] lastObject]; +} + ++ (NSArray *)binRangesForBrand:(STPCardBrand)brand { + return [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return range.brand == brand; + }]]; +} + +@end diff --git a/TelegramUI/STPBackendAPIAdapter.h b/TelegramUI/STPBackendAPIAdapter.h new file mode 100755 index 0000000000..9ce54dec20 --- /dev/null +++ b/TelegramUI/STPBackendAPIAdapter.h @@ -0,0 +1,65 @@ +// +// STPBackendAPIAdapter.h +// Stripe +// +// Created by Jack Flintermann on 1/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +#import "STPAddress.h" +#import "STPBlocks.h" +#import "STPSource.h" +#import "STPCustomer.h" + +NS_ASSUME_NONNULL_BEGIN + +@class STPCard, STPToken; + +/** + * Call this block after you're done fetching a customer on your server. You can use the `STPCustomerDeserializer` class to convert a JSON response into an `STPCustomer` object. + * + * @param customer a deserialized `STPCustomer` object obtained from your backend API, or nil if an error occurred. + * @param error any error that occurred while communicating with your server, or nil if your call succeeded + */ +typedef void (^STPCustomerCompletionBlock)(STPCustomer * __nullable customer, NSError * __nullable error); + +/** + * You should make your application's API client conform to this interface in order to use it with an `STPPaymentContext`. It provides a "bridge" from the prebuilt UI we expose (such as `STPPaymentMethodsViewController`) to your backend to fetch the information it needs to power those views. To read about how to implement this protocol, see https://stripe.com/docs/mobile/ios#prepare-your-api . To see examples of implementing these APIs, see MyAPIClient.swift in our example project and https://github.com/stripe/example-ios-backend . + */ +@protocol STPBackendAPIAdapter + +/** + * Retrieve the cards to be displayed inside a payment context. On your backend, retrieve the Stripe customer associated with your currently logged-in user (see https://stripe.com/docs/api#retrieve_customer ), and return the raw JSON response from the Stripe API. (For an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L40 ). Back in your iOS app, after you've called this API, deserialize your API response into an `STPCustomer` object (you can use the `STPCustomerDeserializer` class to do this). See MyAPIClient.swift in our example project to see this in action. + * + * @see STPCard + * @param completion call this callback when you're done fetching and parsing the above information from your backend. For example, `completion(customer, nil)` (if your call succeeds) or `completion(nil, error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)retrieveCustomer:(STPCustomerCompletionBlock)completion; + +/** + * Adds a payment source to a customer. On your backend, retrieve the Stripe customer associated with your logged-in user. Then, call the Update Customer method on that customer as described at https://stripe.com/docs/api#update_customer (for an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L60 ). If this API call succeeds, call `completion(nil)`. Otherwise, call `completion(error)` with the error that occurred. + * + * @param source a valid payment source, such as a card token. + * @param completion call this callback when you're done adding the token to the customer on your backend. For example, `completion(nil)` (if your call succeeds) or `completion(error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)attachSourceToCustomer:(id)source completion:(STPErrorBlock)completion; + +/** + * Change a customer's `default_source` to be the provided card. On your backend, retrieve the Stripe customer associated with your logged-in user. Then, call the Customer Update method as described at https://stripe.com/docs/api#update_customer , specifying default_source to be the value of source.stripeID (for an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L82 ). If this API call succeeds, call `completion(nil)`. Otherwise, call `completion(error)` with the error that occurred. + * + * @param source The newly-selected default source for the user. + * @param completion call this callback when you're done selecting the new default source for the customer on your backend. For example, `completion(nil)` (if your call succeeds) or `completion(error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)selectDefaultCustomerSource:(id)source completion:(STPErrorBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPBankAccount.h b/TelegramUI/STPBankAccount.h new file mode 100755 index 0000000000..0e18773d83 --- /dev/null +++ b/TelegramUI/STPBankAccount.h @@ -0,0 +1,98 @@ +// +// STPBankAccount.h +// Stripe +// +// Created by Charles Scalesse on 10/1/14. +// +// + +#import +#import "STPBankAccountParams.h" +#import "STPAPIResponseDecodable.h" + +typedef NS_ENUM(NSInteger, STPBankAccountStatus) { + STPBankAccountStatusNew, + STPBankAccountStatusValidated, + STPBankAccountStatusVerified, + STPBankAccountStatusErrored, +}; + +/** + * Representation of a user's bank account details that have been tokenized with the Stripe API. @see https://stripe.com/docs/api#cards + */ +@interface STPBankAccount : STPBankAccountParams + +/** + * The last 4 digits of the bank account's account number. + */ +- (nonnull NSString *)last4; + +/** + * The routing number for the bank account. This should be the ACH routing number, not the wire routing number. + */ +@property (nonatomic, copy, nonnull) NSString *routingNumber; + +/** + * Two-letter ISO code representing the country the bank account is located in. + */ +@property (nonatomic, copy, nullable) NSString *country; + +/** + * The default currency for the bank account. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * The Stripe ID for the bank account. + */ +@property (nonatomic, readonly, nonnull) NSString *bankAccountId; + +/** + * The last 4 digits of the account number. + */ +@property (nonatomic, readonly, nullable) NSString *last4; + +/** + * The name of the bank that owns the account. + */ +@property (nonatomic, readonly, nullable) NSString *bankName; + +/** + * The name of the person or business that owns the bank account. + */ +@property(nonatomic, copy, nullable) NSString *accountHolderName; + +/** + * The type of entity that holds the account. + */ +@property(nonatomic) STPBankAccountHolderType accountHolderType; + +/** + * A proxy for the account number, this uniquely identifies the account and can be used to compare equality of different bank accounts. + */ +@property (nonatomic, readonly, nullable) NSString *fingerprint; + +/** + * The validation status of the bank account. @see STPBankAccountStatus + */ +@property (nonatomic, readonly) STPBankAccountStatus status; + +/** + * Whether or not the bank account has been validated via microdeposits or other means. + * @deprecated Use status == STPBankAccountStatusValidated instead. + */ +@property (nonatomic, readonly) BOOL validated __attribute__((deprecated("Use status == STPBankAccountStatusValidated instead."))); + +/** + * Whether or not the bank account is currently disabled. + * @deprecated Use status == STPBankAccountStatusErrored instead. + */ +@property (nonatomic, readonly) BOOL disabled __attribute__((deprecated("Use status == STPBankAccountStatusErrored instead."))); + +#pragma mark - deprecated setters for STPBankAccountParams properties + +#define DEPRECATED_IN_FAVOR_OF_STPBANKACCOUNTPARAMS __attribute__((deprecated("For collecting your users' bank account details, you should use an STPBankAccountParams object instead of an STPBankAccount."))) + +- (void)setAccountNumber:(nullable NSString *)accountNumber DEPRECATED_IN_FAVOR_OF_STPBANKACCOUNTPARAMS; + +@end diff --git a/TelegramUI/STPBankAccount.m b/TelegramUI/STPBankAccount.m new file mode 100755 index 0000000000..565a244844 --- /dev/null +++ b/TelegramUI/STPBankAccount.m @@ -0,0 +1,113 @@ +// +// STPBankAccount.m +// Stripe +// +// Created by Charles Scalesse on 10/1/14. +// +// + +#import "STPBankAccount.h" +#import "NSDictionary+Stripe.h" + +@interface STPBankAccount () + +@property (nonatomic, readwrite) NSString *bankAccountId; +@property (nonatomic, readwrite) NSString *last4; +@property (nonatomic, readwrite) NSString *bankName; +@property (nonatomic, readwrite) NSString *fingerprint; +@property (nonatomic) STPBankAccountStatus status; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; + +@end + +@implementation STPBankAccount + +@synthesize routingNumber, country, currency, accountHolderName, accountHolderType; + +- (void)setAccountNumber:(NSString *)accountNumber { + [super setAccountNumber:accountNumber]; +} + +- (NSString *)last4 { + return _last4 ?: [super last4]; +} + +#pragma mark - Equality + +- (BOOL)isEqual:(STPBankAccount *)bankAccount { + return [self isEqualToBankAccount:bankAccount]; +} + +- (NSUInteger)hash { + return [self.bankAccountId hash]; +} + +- (BOOL)isEqualToBankAccount:(STPBankAccount *)bankAccount { + if (self == bankAccount) { + return YES; + } + + if (!bankAccount || ![bankAccount isKindOfClass:self.class]) { + return NO; + } + + return [self.bankAccountId isEqualToString:bankAccount.bankAccountId]; +} + +- (BOOL)validated { + return self.status == STPBankAccountStatusValidated; +} + +- (BOOL)disabled { + return self.status == STPBankAccountStatusErrored; +} + +#pragma mark STPAPIResponseDecodable + ++ (NSArray *)requiredFields { + return @[ + @"id", + @"last4", + @"bank_name", + @"country", + @"currency", + @"status", + ]; +} + ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPBankAccount *bankAccount = [self new]; + bankAccount.bankAccountId = dict[@"id"]; + bankAccount.last4 = dict[@"last4"]; + bankAccount.bankName = dict[@"bank_name"]; + bankAccount.country = dict[@"country"]; + bankAccount.fingerprint = dict[@"fingerprint"]; + bankAccount.currency = dict[@"currency"]; + bankAccount.accountHolderName = dict[@"account_holder_name"]; + NSString *accountHolderType = dict[@"account_holder_type"]; + if ([accountHolderType isEqualToString:@"individual"]) { + bankAccount.accountHolderType = STPBankAccountHolderTypeIndividual; + } else if ([accountHolderType isEqualToString:@"company"]) { + bankAccount.accountHolderType = STPBankAccountHolderTypeCompany; + } + NSString *status = dict[@"status"]; + if ([status isEqual: @"new"]) { + bankAccount.status = STPBankAccountStatusNew; + } else if ([status isEqual: @"validated"]) { + bankAccount.status = STPBankAccountStatusValidated; + } else if ([status isEqual: @"verified"]) { + bankAccount.status = STPBankAccountStatusVerified; + } else if ([status isEqual: @"errored"]) { + bankAccount.status = STPBankAccountStatusErrored; + } + + bankAccount.allResponseFields = dict; + return bankAccount; +} + +@end diff --git a/TelegramUI/STPBankAccountParams.h b/TelegramUI/STPBankAccountParams.h new file mode 100755 index 0000000000..9d22c4e71d --- /dev/null +++ b/TelegramUI/STPBankAccountParams.h @@ -0,0 +1,58 @@ +// +// STPBankAccountParams.h +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import "STPFormEncodable.h" + +typedef NS_ENUM(NSInteger, STPBankAccountHolderType) { + STPBankAccountHolderTypeIndividual, + STPBankAccountHolderTypeCompany, +}; + +/** + * Representation of a user's bank account details. You can assemble these with information that your user enters and + * then create Stripe tokens with them using an STPAPIClient. @see https://stripe.com/docs/api#create_bank_account_token + */ +@interface STPBankAccountParams : NSObject + +/** + * The account number for the bank account. Currently must be a checking account. + */ +@property (nonatomic, copy, nullable) NSString *accountNumber; + +/** + * The last 4 digits of the bank account's account number, if it's been set, otherwise nil. + */ +- (nullable NSString *)last4; + +/** + * The routing number for the bank account. This should be the ACH routing number, not the wire routing number. + */ +@property (nonatomic, copy, nullable) NSString *routingNumber; + +/** + * Two-letter ISO code representing the country the bank account is located in. + */ +@property (nonatomic, copy, nullable) NSString *country; + +/** + * The default currency for the bank account. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * The name of the person or business that owns the bank account. + */ +@property(nonatomic, copy, nullable) NSString *accountHolderName; + +/** + * The type of entity that holds the account. Defaults to STPBankAccountHolderTypeIndividual. + */ +@property(nonatomic) STPBankAccountHolderType accountHolderType; + +@end diff --git a/TelegramUI/STPBankAccountParams.m b/TelegramUI/STPBankAccountParams.m new file mode 100755 index 0000000000..2be5fc537e --- /dev/null +++ b/TelegramUI/STPBankAccountParams.m @@ -0,0 +1,61 @@ +// +// STPBankAccountParams.m +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPBankAccountParams.h" +#define FAUXPAS_IGNORED_ON_LINE(...) + +@interface STPBankAccountParams() +@property(nonatomic, readonly)NSString *accountHolderTypeString; +@end + +@implementation STPBankAccountParams + +@synthesize additionalAPIParameters = _additionalAPIParameters; + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalAPIParameters = @{}; + _accountHolderType = STPBankAccountHolderTypeIndividual; + } + return self; +} + +- (NSString *)last4 { + if (self.accountNumber && self.accountNumber.length >= 4) { + return [self.accountNumber substringFromIndex:(self.accountNumber.length - 4)]; + } else { + return nil; + } +} + +- (NSString *)accountHolderTypeString { FAUXPAS_IGNORED_ON_LINE(UnusedMethod) + switch (self.accountHolderType) { + case STPBankAccountHolderTypeCompany: + return @"company"; + case STPBankAccountHolderTypeIndividual: + return @"individual"; + } +} + ++ (NSString *)rootObjectName { + return @"bank_account"; +} + ++ (NSDictionary *)propertyNamesToFormFieldNamesMapping { + return @{ + @"accountNumber": @"account_number", + @"routingNumber": @"routing_number", + @"country": @"country", + @"currency": @"currency", + @"accountHolderName": @"account_holder_name", + @"accountHolderTypeString": @"account_holder_type", + }; +} + +@end diff --git a/TelegramUI/STPBlocks.h b/TelegramUI/STPBlocks.h new file mode 100755 index 0000000000..9241997934 --- /dev/null +++ b/TelegramUI/STPBlocks.h @@ -0,0 +1,49 @@ +// +// STPBlocks.h +// Stripe +// +// Created by Jack Flintermann on 3/23/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +@class STPToken; + +/** + * An enum representing the status of a payment requested from the user. + */ +typedef NS_ENUM(NSUInteger, STPPaymentStatus) { + /** + * The payment succeeded. + */ + STPPaymentStatusSuccess, + /** + * The payment failed due to an unforeseen error, such as the user's Internet connection being offline. + */ + STPPaymentStatusError, + /** + * The user cancelled the payment (for example, by hitting "cancel" in the Apple Pay dialog). + */ + STPPaymentStatusUserCancellation, +}; + +/** + * An empty block, called with no arguments, returning nothing. + */ +typedef void (^STPVoidBlock)(); + +/** + * A block that may optionally be called with an error. + * + * @param error The error that occurred, if any. + */ +typedef void (^STPErrorBlock)(NSError * __nullable error); + +/** + * A callback to be run with a token response from the Stripe API. + * + * @param token The Stripe token from the response. Will be nil if an error occurs. @see STPToken + * @param error The error returned from the response, or nil in one occurs. @see StripeError.h for possible values. + */ +typedef void (^STPTokenCompletionBlock)(STPToken * __nullable token, NSError * __nullable error); diff --git a/TelegramUI/STPCard.h b/TelegramUI/STPCard.h new file mode 100755 index 0000000000..efd3b4e456 --- /dev/null +++ b/TelegramUI/STPCard.h @@ -0,0 +1,168 @@ +// +// STPCard.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/2/12. +// +// + +#import + +#import "STPCardBrand.h" +#import "STPCardParams.h" +#import "STPAPIResponseDecodable.h" +#import "STPPaymentMethod.h" +#import "STPSource.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The various funding sources for a payment card. + */ +typedef NS_ENUM(NSInteger, STPCardFundingType) { + STPCardFundingTypeDebit, + STPCardFundingTypeCredit, + STPCardFundingTypePrepaid, + STPCardFundingTypeOther, +}; + +/** + * Representation of a user's credit card details that have been tokenized with the Stripe API. @see https://stripe.com/docs/api#cards + */ +@interface STPCard : STPCardParams + +/** + * Create an STPCard from a Stripe API response. + * + * @param cardID The Stripe ID of the card, e.g. `card_185iQx4JYtv6MPZKfcuXwkOx` + * @param brand The brand of the card (e.g. "Visa". To obtain this enum value from a string, use `[STPCardBrand brandFromString:string]`; + * @param last4 The last 4 digits of the card, e.g. 4242 + * @param expMonth The card's expiration month, 1-indexed (i.e. 1 = January) + * @param expYear The card's expiration year + * @param funding The card's funding type (credit, debit, or prepaid). To obtain this enum value from a string, use `[STPCardBrand fundingFromString:string]`. + * + * @return an STPCard instance populated with the provided values. + */ +- (instancetype)initWithID:(NSString *)cardID + brand:(STPCardBrand)brand + last4:(NSString *)last4 + expMonth:(NSUInteger)expMonth + expYear:(NSUInteger)expYear + funding:(STPCardFundingType)funding; + +/** + * This parses a string representing a card's brand into the appropriate STPCardBrand enum value, i.e. `[STPCard brandFromString:@"American Express"] == STPCardBrandAmex` + * + * @param string a string representing the card's brand as returned from the Stripe API + * + * @return an enum value mapped to that string. If the string is unrecognized, returns STPCardBrandUnknown. + */ ++ (STPCardBrand)brandFromString:(NSString *)string; + +/** + * This parses a string representing a card's funding type into the appropriate `STPCardFundingType` enum value, i.e. `[STPCard fundingFromString:@"prepaid"] == STPCardFundingTypePrepaid`. + * + * @param string a string representing the card's funding type as returned from the Stripe API + * + * @return an enum value mapped to that string. If the string is unrecognized, returns `STPCardFundingTypeOther`. + */ ++ (STPCardFundingType)fundingFromString:(NSString *)string; + +/** + * The last 4 digits of the card. + */ +@property (nonatomic, readonly) NSString *last4; + +/** + * For cards made with Apple Pay, this refers to the last 4 digits of the "Device Account Number" for the tokenized card. For regular cards, it will be nil. + */ +@property (nonatomic, readonly, nullable) NSString *dynamicLast4; + +/** + * Whether or not the card originated from Apple Pay. + */ +@property (nonatomic, readonly) BOOL isApplePayCard; + +/** + * The card's expiration month. 1-indexed (i.e. 1 == January) + */ +@property (nonatomic) NSUInteger expMonth; + +/** + * The card's expiration year. + */ +@property (nonatomic) NSUInteger expYear; + +/** + * The cardholder's name. + */ +@property (nonatomic, copy, nullable) NSString *name; + +/** + * The cardholder's address. + */ +@property (nonatomic, copy, nullable) NSString *addressLine1; +@property (nonatomic, copy, nullable) NSString *addressLine2; +@property (nonatomic, copy, nullable) NSString *addressCity; +@property (nonatomic, copy, nullable) NSString *addressState; +@property (nonatomic, copy, nullable) NSString *addressZip; +@property (nonatomic, copy, nullable) NSString *addressCountry; + +/** + * The Stripe ID for the card. + */ +@property (nonatomic, readonly, nullable) NSString *cardId; + +/** + * The issuer of the card. + */ +@property (nonatomic, readonly) STPCardBrand brand; + +/** + * The issuer of the card. + * Can be one of "Visa", "American Express", "MasterCard", "Discover", "JCB", "Diners Club", or "Unknown" + * @deprecated use `brand` instead. + */ +@property (nonatomic, readonly) NSString *type __attribute__((deprecated)); + +/** + * The funding source for the card (credit, debit, prepaid, or other) + */ +@property (nonatomic, readonly) STPCardFundingType funding; + +/** + * A proxy for the card's number, this uniquely identifies the credit card and can be used to compare different cards. + * @deprecated This field will no longer be present in responses when using your publishable key. If you want to access the value of this field, you can look it up on your backend using your secret key. + */ +@property (nonatomic, readonly, nullable) NSString *fingerprint __attribute__((deprecated("This field will no longer be present in responses when using your publishable key. If you want to access the value of this field, you can look it up on your backend using your secret key."))); + +/** + * Two-letter ISO code representing the issuing country of the card. + */ +@property (nonatomic, readonly, nullable) NSString *country; + +/** + * This is only applicable when tokenizing debit cards to issue payouts to managed accounts. You should not set it otherwise. The card can then be used as a transfer destination for funds in this currency. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +#pragma mark - deprecated properties + +#define DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS __attribute__((deprecated("For collecting your users' credit card details, you should use an STPCardParams object instead of an STPCard."))) + +@property (nonatomic, copy, nullable) NSString *number DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +@property (nonatomic, copy, nullable) NSString *cvc DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setExpMonth:(NSUInteger)expMonth DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setExpYear:(NSUInteger)expYear DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setName:(nullable NSString *)name DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressLine1:(nullable NSString *)addressLine1 DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressLine2:(nullable NSString *)addressLine2 DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressCity:(nullable NSString *)addressCity DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressState:(nullable NSString *)addressState DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressZip:(nullable NSString *)addressZip DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressCountry:(nullable NSString *)addressCountry DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPCard.m b/TelegramUI/STPCard.m new file mode 100755 index 0000000000..da287f4b4a --- /dev/null +++ b/TelegramUI/STPCard.m @@ -0,0 +1,200 @@ +// +// STPCard.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/2/12. +// +// + +#import "STPCard.h" +#import "NSDictionary+Stripe.h" +#import "NSString+Stripe_CardBrands.h" +#import "STPImageLibrary.h" +#import "STPImageLibrary+Private.h" + +@interface STPCard () + +@property (nonatomic, readwrite) NSString *cardId; +@property (nonatomic, readwrite) NSString *last4; +@property (nonatomic, readwrite) NSString *dynamicLast4; +@property (nonatomic, readwrite) STPCardBrand brand; +@property (nonatomic, readwrite) STPCardFundingType funding; +@property (nonatomic, readwrite) NSString *fingerprint; +@property (nonatomic, readwrite) NSString *country; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; + +@end + +@implementation STPCard + +@dynamic number, cvc, expMonth, expYear, currency, name, addressLine1, addressLine2, addressCity, addressState, addressZip, addressCountry; + +- (instancetype)initWithID:(NSString *)stripeID + brand:(STPCardBrand)brand + last4:(NSString *)last4 + expMonth:(NSUInteger)expMonth + expYear:(NSUInteger)expYear + funding:(STPCardFundingType)funding { + self = [super init]; + if (self) { + _cardId = stripeID; + _brand = brand; + _last4 = last4; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + self.expMonth = expMonth; + self.expYear = expYear; +#pragma clang diagnostic pop + _funding = funding; + } + return self; +} + ++ (STPCardBrand)brandFromString:(NSString *)string { + NSString *brand = [string lowercaseString]; + if ([brand isEqualToString:@"visa"]) { + return STPCardBrandVisa; + } else if ([brand isEqualToString:@"american express"]) { + return STPCardBrandAmex; + } else if ([brand isEqualToString:@"mastercard"]) { + return STPCardBrandMasterCard; + } else if ([brand isEqualToString:@"discover"]) { + return STPCardBrandDiscover; + } else if ([brand isEqualToString:@"jcb"]) { + return STPCardBrandJCB; + } else if ([brand isEqualToString:@"diners club"]) { + return STPCardBrandDinersClub; + } else { + return STPCardBrandUnknown; + } +} + ++ (STPCardFundingType)fundingFromString:(NSString *)string { + NSString *funding = [string lowercaseString]; + if ([funding isEqualToString:@"credit"]) { + return STPCardFundingTypeCredit; + } else if ([funding isEqualToString:@"debit"]) { + return STPCardFundingTypeDebit; + } else if ([funding isEqualToString:@"prepaid"]) { + return STPCardFundingTypePrepaid; + } else { + return STPCardFundingTypeOther; + } +} + +- (instancetype)init { + self = [super init]; + if (self) { + _brand = STPCardBrandUnknown; + _funding = STPCardFundingTypeOther; + } + + return self; +} + +- (NSString *)last4 { + return _last4 ?: [super last4]; +} + +- (BOOL)isApplePayCard { + return [self.allResponseFields[@"tokenization_method"] isEqualToString:@"apple_pay"]; +} + +- (NSString *)type { + switch (self.brand) { + case STPCardBrandAmex: + return @"American Express"; + case STPCardBrandDinersClub: + return @"Diners Club"; + case STPCardBrandDiscover: + return @"Discover"; + case STPCardBrandJCB: + return @"JCB"; + case STPCardBrandMasterCard: + return @"MasterCard"; + case STPCardBrandVisa: + return @"Visa"; + default: + return @"Unknown"; + } +} + +- (BOOL)isEqual:(id)other { + return [self isEqualToCard:other]; +} + +- (NSUInteger)hash { + return [self.cardId hash]; +} + +- (BOOL)isEqualToCard:(STPCard *)other { + if (self == other) { + return YES; + } + + if (!other || ![other isKindOfClass:self.class]) { + return NO; + } + + return [self.cardId isEqualToString:other.cardId]; +} + +#pragma mark STPAPIResponseDecodable ++ (NSArray *)requiredFields { + return @[@"id", @"last4", @"brand", @"exp_month", @"exp_year"]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPCard *card = [self new]; + card.cardId = dict[@"id"]; + card.name = dict[@"name"]; + card.last4 = dict[@"last4"]; + card.dynamicLast4 = dict[@"dynamic_last4"]; + NSString *brand = [dict[@"brand"] lowercaseString]; + card.brand = [self.class brandFromString:brand]; + NSString *funding = dict[@"funding"]; + card.funding = [self.class fundingFromString:funding]; + card.fingerprint = dict[@"fingerprint"]; + card.country = dict[@"country"]; + card.currency = dict[@"currency"]; + card.expMonth = [dict[@"exp_month"] intValue]; + card.expYear = [dict[@"exp_year"] intValue]; + card.addressLine1 = dict[@"address_line1"]; + card.addressLine2 = dict[@"address_line2"]; + card.addressCity = dict[@"address_city"]; + card.addressState = dict[@"address_state"]; + card.addressZip = dict[@"address_zip"]; + card.addressCountry = dict[@"address_country"]; + + card.allResponseFields = dict; + return card; +} +#pragma clang diagnostic pop + +#pragma mark - STPSource + +- (NSString *)stripeID { + return self.cardId; +} + +- (NSString *)label { + NSString *brand = [NSString stp_stringWithCardBrand:self.brand]; + return [NSString stringWithFormat:@"%@ %@", brand, self.last4]; +} + +- (UIImage *)image { + return [STPImageLibrary brandImageForCardBrand:self.brand]; +} + +- (UIImage *)templateImage { + return [STPImageLibrary templatedBrandImageForCardBrand:self.brand]; +} + +@end diff --git a/TelegramUI/STPCardBrand.h b/TelegramUI/STPCardBrand.h new file mode 100755 index 0000000000..cc4390ccd3 --- /dev/null +++ b/TelegramUI/STPCardBrand.h @@ -0,0 +1,22 @@ +// +// STPCardBrand.h +// Stripe +// +// Created by Jack Flintermann on 7/24/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * The various card brands to which a payment card can belong. + */ +typedef NS_ENUM(NSInteger, STPCardBrand) { + STPCardBrandVisa, + STPCardBrandAmex, + STPCardBrandMasterCard, + STPCardBrandDiscover, + STPCardBrandJCB, + STPCardBrandDinersClub, + STPCardBrandUnknown, +}; diff --git a/TelegramUI/STPCardParams.h b/TelegramUI/STPCardParams.h new file mode 100755 index 0000000000..f1cf06d03f --- /dev/null +++ b/TelegramUI/STPCardParams.h @@ -0,0 +1,98 @@ +// +// STPCardParams.h +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import "STPFormEncodable.h" +#if TARGET_OS_IPHONE +#import "STPAddress.h" +#endif + +/** + * Representation of a user's credit card details. You can assemble these with information that your user enters and + * then create Stripe tokens with them using an STPAPIClient. @see https://stripe.com/docs/api#cards + */ +@interface STPCardParams : NSObject + +/** + * The card's number. + */ +@property (nonatomic, copy, nullable) NSString *number; + +/** + * The last 4 digits of the card's number, if it's been set, otherwise nil. + */ +- (nullable NSString *)last4; + +/** + * The card's expiration month. + */ +@property (nonatomic) NSUInteger expMonth; + +/** + * The card's expiration year. + */ +@property (nonatomic) NSUInteger expYear; + +/** + * The card's security code, found on the back. + */ +@property (nonatomic, copy, nullable) NSString *cvc; + +/** + * The cardholder's name. + */ +@property (nonatomic, copy, nullable) NSString *name; + +/** + * The cardholder's address. + */ +#if TARGET_OS_IPHONE +@property(nonatomic, copy, nonnull) STPAddress *address; +#endif + +@property (nonatomic, copy, nullable) NSString *addressLine1; +@property (nonatomic, copy, nullable) NSString *addressLine2; +@property (nonatomic, copy, nullable) NSString *addressCity; +@property (nonatomic, copy, nullable) NSString *addressState; +@property (nonatomic, copy, nullable) NSString *addressZip; +@property (nonatomic, copy, nullable) NSString *addressCountry; + +/** + * Three-letter ISO currency code representing the currency paid out to the bank account. This is only applicable when tokenizing debit cards to issue payouts to managed accounts. You should not set it otherwise. The card can then be used as a transfer destination for funds in this currency. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * Validate each field of the card. + * @return whether or not that field is valid. + * @deprecated use STPCardValidator instead. + */ +- (BOOL)validateNumber:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateCvc:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateExpMonth:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateExpYear:(__nullable id * __nullable)ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); + +/** + * This validates a fully populated card to check for all errors, including ones that come about + * from the interaction of more than one property. It will also do all the validations on individual + * properties, so if you only want to call one method on your card to validate it after setting all the + * properties, call this one + * + * @param outError a pointer to an NSError that, after calling this method, will be populated with an error if the card is not valid. See StripeError.h for + possible values + * + * @return whether or not the card is valid. + * @deprecated use STPCardValidator instead. + */ +- (BOOL)validateCardReturningError:(NSError * __nullable * __nullable)outError __attribute__((deprecated("Use STPCardValidator instead."))); + +@end diff --git a/TelegramUI/STPCardParams.m b/TelegramUI/STPCardParams.m new file mode 100755 index 0000000000..329271d0b3 --- /dev/null +++ b/TelegramUI/STPCardParams.m @@ -0,0 +1,199 @@ +// +// STPCardParams.m +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPCardParams.h" +#import "STPCardValidator.h" +#import "StripeError.h" + +@implementation STPCardParams + +@synthesize additionalAPIParameters = _additionalAPIParameters; + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalAPIParameters = @{}; + } + return self; +} + +- (NSString *)last4 { + if (self.number && self.number.length >= 4) { + return [self.number substringFromIndex:(self.number.length - 4)]; + } else { + return nil; + } +} + +#if TARGET_OS_IPHONE + +- (STPAddress *)address { + STPAddress *address = [STPAddress new]; + address.name = self.name; + address.line1 = self.addressLine1; + address.line2 = self.addressLine2; + address.city = self.addressCity; + address.state = self.addressState; + address.postalCode = self.addressZip; + address.country = self.addressCountry; + return address; +} + +- (void)setAddress:(STPAddress *)address { + self.name = address.name; + self.addressLine1 = address.line1; + self.addressLine2 = address.line2; + self.addressCity = address.city; + self.addressState = address.state; + self.addressZip = address.postalCode; + self.addressCountry = address.country; +} + +#endif + +- (BOOL)validateNumber:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + NSString *ioValueString = (NSString *)*ioValue; + + if ([STPCardValidator validationStateForNumber:ioValueString validatingCardBrand:NO] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + return YES; +} + +- (BOOL)validateCvc:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + NSString *ioValueString = (NSString *)*ioValue; + + STPCardBrand brand = [STPCardValidator brandForNumber:self.number]; + + if ([STPCardValidator validationStateForCVC:ioValueString cardBrand:brand] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"cvc" error:outError]; + } + return YES; +} + +- (BOOL)validateExpMonth:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"expMonth" error:outError]; + } + NSString *ioValueString = [(NSString *)*ioValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if ([STPCardValidator validationStateForExpirationMonth:ioValueString] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"expMonth" error:outError]; + } + return YES; +} + +- (BOOL)validateExpYear:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"expYear" error:outError]; + } + NSString *ioValueString = [(NSString *)*ioValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + NSString *monthString = [@(self.expMonth) stringValue]; + if ([STPCardValidator validationStateForExpirationYear:ioValueString inMonth:monthString] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"expYear" error:outError]; + } + return YES; +} + +- (BOOL)validateCardReturningError:(NSError **)outError { + // Order matters here + NSString *numberRef = [self number]; + NSString *expMonthRef = [NSString stringWithFormat:@"%02lu", (unsigned long)[self expMonth]]; + NSString *expYearRef = [NSString stringWithFormat:@"%02lu", (unsigned long)[self expYear]]; + NSString *cvcRef = [self cvc]; + + // Make sure expMonth, expYear, and number are set. Validate CVC if it is provided + return [self validateNumber:&numberRef error:outError] && [self validateExpYear:&expYearRef error:outError] && + [self validateExpMonth:&expMonthRef error:outError] && (cvcRef == nil || [self validateCvc:&cvcRef error:outError]); +} + +#pragma mark Private Helpers ++ (BOOL)handleValidationErrorForParameter:(NSString *)parameter error:(NSError **)outError { + if (outError != nil) { + if ([parameter isEqualToString:@"number"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidNumberUserMessage] + parameter:parameter + cardErrorCode:STPInvalidNumber + devErrorMessage:@"Card number must be between 10 and 19 digits long and Luhn valid."]; + } else if ([parameter isEqualToString:@"cvc"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardInvalidCVCUserMessage] + parameter:parameter + cardErrorCode:STPInvalidCVC + devErrorMessage:@"Card CVC must be numeric, 3 digits for Visa, Discover, MasterCard, JCB, and Discover cards, and 3 or 4 " + @"digits for American Express cards."]; + } else if ([parameter isEqualToString:@"expMonth"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidExpMonthUserMessage] + parameter:parameter + cardErrorCode:STPInvalidExpMonth + devErrorMessage:@"expMonth must be less than 13"]; + } else if ([parameter isEqualToString:@"expYear"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidExpYearUserMessage] + parameter:parameter + cardErrorCode:STPInvalidExpYear + devErrorMessage:@"expYear must be this year or a year in the future"]; + } else { + // This should not be possible since this is a private method so we + // know exactly how it is called. We use STPAPIError for all errors + // that are unexpected within the bindings as well. + *outError = [[NSError alloc] initWithDomain:StripeDomain + code:STPAPIError + userInfo:@{ + NSLocalizedDescriptionKey: [NSError stp_unexpectedErrorMessage], + STPErrorMessageKey: @"There was an error within the Stripe client library when trying to generate the " + @"proper validation error. Contact support@stripe.com if you see this." + }]; + } + } + return NO; +} + ++ (NSError *)createErrorWithMessage:(NSString *)userMessage + parameter:(NSString *)parameter + cardErrorCode:(NSString *)cardErrorCode + devErrorMessage:(NSString *)devMessage { + return [[NSError alloc] initWithDomain:StripeDomain + code:STPCardError + userInfo:@{ + NSLocalizedDescriptionKey: userMessage, + STPErrorParameterKey: parameter, + STPCardErrorCodeKey: cardErrorCode, + STPErrorMessageKey: devMessage + }]; +} + +#pragma mark - STPFormEncodable + ++ (NSString *)rootObjectName { + return @"card"; +} + ++ (NSDictionary *)propertyNamesToFormFieldNamesMapping { + return @{ + @"number": @"number", + @"cvc": @"cvc", + @"name": @"name", + @"addressLine1": @"address_line1", + @"addressLine2": @"address_line2", + @"addressCity": @"address_city", + @"addressState": @"address_state", + @"addressZip": @"address_zip", + @"addressCountry": @"address_country", + @"expMonth": @"exp_month", + @"expYear": @"exp_year", + @"currency": @"currency", + }; +} + +@end diff --git a/TelegramUI/STPCardValidationState.h b/TelegramUI/STPCardValidationState.h new file mode 100755 index 0000000000..7efc4870ca --- /dev/null +++ b/TelegramUI/STPCardValidationState.h @@ -0,0 +1,18 @@ +// +// STPCardValidationState.h +// Stripe +// +// Created by Jack Flintermann on 8/7/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * These fields indicate whether a card field represents a valid value, invalid value, or incomplete value. + */ +typedef NS_ENUM(NSInteger, STPCardValidationState) { + STPCardValidationStateValid, // The field's contents are valid. For example, a valid, 16-digit card number. + STPCardValidationStateInvalid, // The field's contents are invalid. For example, an expiration date of "13/42". + STPCardValidationStateIncomplete, // The field's contents are not yet valid, but could be by typing additional characters. For example, a CVC of "1". +}; diff --git a/TelegramUI/STPCardValidator.h b/TelegramUI/STPCardValidator.h new file mode 100755 index 0000000000..b01a224802 --- /dev/null +++ b/TelegramUI/STPCardValidator.h @@ -0,0 +1,119 @@ +// +// STPCardValidator.h +// Stripe +// +// Created by Jack Flintermann on 7/15/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +#import "STPCardParams.h" +#import "STPCardBrand.h" +#import "STPCardValidationState.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class contains static methods to validate card numbers, expiration dates, and CVCs. For a list of test card numbers to use with this code, see https://stripe.com/docs/testing + */ +@interface STPCardValidator : NSObject + +/** + * Returns a copy of the passed string with all non-numeric characters removed. + */ ++ (NSString *)sanitizedNumericStringForString:(NSString *)string; + +/** + * Whether or not the target string contains only numeric characters. + */ ++ (BOOL)stringIsNumeric:(NSString *)string; + +/** + * Validates a card number, passed as a string. This will return STPCardValidationStateInvalid for numbers that are too short or long, contain invalid characters, do not pass Luhn validation, or (optionally) do not match a number format issued by a major card brand. + * + * @param cardNumber The card number to validate. Ex. @"4242424242424242" + * @param validatingCardBrand Whether or not to enforce that the number appears to be issued by a major card brand (or could be). For example, no issuing card network currently issues card numbers beginning with the digit 9; if an otherwise correct-length and luhn-valid card number beginning with 9 (example: 9999999999999995) were passed to this method, it would return STPCardValidationStateInvalid if this parameter were YES and STPCardValidationStateValid if this parameter were NO. If unsure, you should use YES for this value. + * + * @return STPCardValidationStateValid if the number is valid, STPCardValidationStateInvalid if the number is invalid, or STPCardValidationStateIncomplete if the number is a substring of a valid card (e.g. @"4242"). + */ ++ (STPCardValidationState)validationStateForNumber:(NSString *)cardNumber + validatingCardBrand:(BOOL)validatingCardBrand; + +/** + * The card brand for a card number or substring thereof. + * + * @param cardNumber A card number, or partial card number. For example, @"4242", @"5555555555554444", or @"123". + * + * @return The brand for that card number. The example parameters would return STPCardBrandVisa, STPCardBrandMasterCard, and STPCardBrandUnknown, respectively. + */ ++ (STPCardBrand)brandForNumber:(NSString *)cardNumber; + +/** + * The possible number lengths for cards associated with a card brand. For example, Discover card numbers contain 16 characters, while American Express cards contain 15 characters. + */ ++ (NSSet*)lengthsForCardBrand:(STPCardBrand)brand; ++ (NSInteger)maxLengthForCardBrand:(STPCardBrand)brand; ++ (NSInteger)lengthForCardBrand:(STPCardBrand)brand __attribute__((deprecated("Card brands may have multiple lengths - use lengthsForCardBrand or maxLengthForCardBrand instead."))); + +/** + * The length of the final grouping of digits to use when formatting a card number for display. For example, Visa cards display their final 4 numbers, e.g. "4242", while American Express cards display their final 5 digits, e.g. "10005". + */ ++ (NSInteger)fragmentLengthForCardBrand:(STPCardBrand)brand; + +/** + * Validates an expiration month, passed as an (optionally 0-padded) string. Example valid values are "3", "12", and "08". Example invalid values are "99", "a", and "00". Incomplete values include "0" and "1". + * + * @param expirationMonth A string representing a 2-digit expiration month for a payment card. + * + * @return STPCardValidationStateValid if the month is valid, STPCardValidationStateInvalid if the month is invalid, or STPCardValidationStateIncomplete if the month is a substring of a valid month (e.g. @"0" or @"1"). + */ ++ (STPCardValidationState)validationStateForExpirationMonth:(NSString *)expirationMonth; + +/** + * Validates an expiration year, passed as a string representing the final 2 digits of the year. This considers the period between the current year until 2099 as valid times. An example valid value would be "16" (assuming the current year, as determined by [NSDate date], is 2015). Will return STPCardValidationStateInvalid for a month/year combination that is earlier than the current date (i.e. @"15" and @"04" in October 2015. Example invalid values are "00", "a", and "13". Any 1-digit string will return STPCardValidationStateIncomplete. + * + * @param expirationYear A string representing a 2-digit expiration year for a payment card. + * @param expirationMonth A string representing a 2-digit expiration month for a payment card. See -validationStateForExpirationMonth for the desired formatting of this string. + * + * @return STPCardValidationStateValid if the year is valid, STPCardValidationStateInvalid if the year is invalid, or STPCardValidationStateIncomplete if the year is a substring of a valid year (e.g. @"1" or @"2"). + */ ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth; + +/** + * The max CVC length for a card brand (for context, American Express CVCs are 4 digits, while all others are 3). + */ ++ (NSUInteger)maxCVCLengthForCardBrand:(STPCardBrand)brand; + +/** + * Validates a card's CVC, passed as a numeric string, for the given card brand. + * + * @param cvc the CVC to validate + * @param brand the card brand (can be determined from the card's number using +brandForNumber) + * + * @return Whether the CVC represents a valid CVC for that card brand. For example, would return STPCardValidationStateValid for @"123" and STPCardBrandVisa, STPCardValidationStateValid for @"1234" and STPCardBrandAmericanExpress, STPCardValidationStateIncomplete for @"12" and STPCardBrandVisa, and STPCardValidationStateInvalid for @"12345" and any brand. + */ ++ (STPCardValidationState)validationStateForCVC:(NSString *)cvc cardBrand:(STPCardBrand)brand; + +/** + * Validates the given card details. + * + * @param card the card details to validate. + * + * @return STPCardValidationStateValid if all fields are valid, STPCardValidationStateInvalid if any field is invalid, or STPCardValidationStateIncomplete if all fields are either incomplete or valid. + */ ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card; + +// Exposed for testing only. ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth + inCurrentYear:(NSInteger)currentYear + currentMonth:(NSInteger)currentMonth; ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card + inCurrentYear:(NSInteger)currentYear + currentMonth:(NSInteger)currentMonth; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPCardValidator.m b/TelegramUI/STPCardValidator.m new file mode 100755 index 0000000000..2d8d5ae371 --- /dev/null +++ b/TelegramUI/STPCardValidator.m @@ -0,0 +1,290 @@ +// +// STPCardValidator.m +// Stripe +// +// Created by Jack Flintermann on 7/15/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPCardValidator.h" +#import "STPBINRange.h" + +@implementation STPCardValidator + ++ (NSString *)sanitizedNumericStringForString:(NSString *)string { + return stringByRemovingCharactersFromSet(string, invertedAsciiDigitCharacterSet()); +} + +static NSCharacterSet *invertedAsciiDigitCharacterSet() { + static NSCharacterSet *cs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cs = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]; + }); + return cs; +} + ++ (NSString *)stringByRemovingSpacesFromString:(NSString *)string { + NSCharacterSet *set = [NSCharacterSet whitespaceCharacterSet]; + return stringByRemovingCharactersFromSet(string, set); +} + +static NSString * _Nonnull stringByRemovingCharactersFromSet(NSString * _Nonnull string, NSCharacterSet * _Nonnull cs) { + NSRange range = [string rangeOfCharacterFromSet:cs]; + if (range.location != NSNotFound) { + NSMutableString *newString = [[string substringWithRange:NSMakeRange(0, range.location)] mutableCopy]; + NSUInteger lastPosition = NSMaxRange(range); + while (lastPosition < string.length) { + range = [string rangeOfCharacterFromSet:cs options:0 range:NSMakeRange(lastPosition, string.length - lastPosition)]; + if (range.location == NSNotFound) break; + if (range.location != lastPosition) { + [newString appendString:[string substringWithRange:NSMakeRange(lastPosition, range.location - lastPosition)]]; + } + lastPosition = NSMaxRange(range); + } + if (lastPosition != string.length) { + [newString appendString:[string substringWithRange:NSMakeRange(lastPosition, string.length - lastPosition)]]; + } + return newString; + } else { + return string; + } +} + ++ (BOOL)stringIsNumeric:(NSString *)string { + return [string rangeOfCharacterFromSet:invertedAsciiDigitCharacterSet()].location == NSNotFound; +} + ++ (STPCardValidationState)validationStateForExpirationMonth:(NSString *)expirationMonth { + + NSString *sanitizedExpiration = [self stringByRemovingSpacesFromString:expirationMonth]; + + if (![self stringIsNumeric:sanitizedExpiration]) { + return STPCardValidationStateInvalid; + } + + switch (sanitizedExpiration.length) { + case 0: + return STPCardValidationStateIncomplete; + case 1: + return ([sanitizedExpiration isEqualToString:@"0"] || [sanitizedExpiration isEqualToString:@"1"]) ? STPCardValidationStateIncomplete : STPCardValidationStateValid; + case 2: + return (0 < sanitizedExpiration.integerValue && sanitizedExpiration.integerValue <= 12) ? STPCardValidationStateValid : STPCardValidationStateInvalid; + default: + return STPCardValidationStateInvalid; + } +} + ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear inMonth:(NSString *)expirationMonth inCurrentYear:(NSInteger)currentYear currentMonth:(NSInteger)currentMonth { + + NSInteger moddedYear = currentYear % 100; + + if (![self stringIsNumeric:expirationMonth] || ![self stringIsNumeric:expirationYear]) { + return STPCardValidationStateInvalid; + } + + NSString *sanitizedMonth = [self sanitizedNumericStringForString:expirationMonth]; + NSString *sanitizedYear = [self sanitizedNumericStringForString:expirationYear]; + + switch (sanitizedYear.length) { + case 0: + case 1: + return STPCardValidationStateIncomplete; + case 2: { + if (sanitizedYear.integerValue == moddedYear) { + return sanitizedMonth.integerValue >= currentMonth ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } else { + return sanitizedYear.integerValue > moddedYear ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } + } + default: + return STPCardValidationStateInvalid; + } +} + + ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth { + return [self validationStateForExpirationYear:expirationYear + inMonth:expirationMonth + inCurrentYear:[self currentYear] + currentMonth:[self currentMonth]]; +} + + ++ (STPCardValidationState)validationStateForCVC:(NSString *)cvc cardBrand:(STPCardBrand)brand { + + if (![self stringIsNumeric:cvc]) { + return STPCardValidationStateInvalid; + } + + NSString *sanitizedCvc = [self sanitizedNumericStringForString:cvc]; + + NSUInteger minLength = [self minCVCLength]; + NSUInteger maxLength = [self maxCVCLengthForCardBrand:brand]; + if (sanitizedCvc.length < minLength) { + return STPCardValidationStateIncomplete; + } + else if (sanitizedCvc.length > maxLength) { + return STPCardValidationStateInvalid; + } + else { + return STPCardValidationStateValid; + } +} + ++ (STPCardValidationState)validationStateForNumber:(nonnull NSString *)cardNumber + validatingCardBrand:(BOOL)validatingCardBrand { + + NSString *sanitizedNumber = [self stringByRemovingSpacesFromString:cardNumber]; + if (![self stringIsNumeric:sanitizedNumber]) { + return STPCardValidationStateInvalid; + } + if (sanitizedNumber.length == 0) { + return STPCardValidationStateIncomplete; + } + STPBINRange *binRange = [STPBINRange mostSpecificBINRangeForNumber:sanitizedNumber]; + if (binRange.brand == STPCardBrandUnknown && validatingCardBrand) { + return STPCardValidationStateInvalid; + } + if (sanitizedNumber.length == binRange.length) { + BOOL isValidLuhn = [self stringIsValidLuhn:sanitizedNumber]; + return isValidLuhn ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } else if (sanitizedNumber.length > binRange.length) { + return STPCardValidationStateInvalid; + } else { + return STPCardValidationStateIncomplete; + } +} + ++ (STPCardValidationState)validationStateForCard:(nonnull STPCardParams *)card inCurrentYear:(NSInteger)currentYear currentMonth:(NSInteger)currentMonth { + STPCardValidationState numberValidation = [self validationStateForNumber:card.number validatingCardBrand:YES]; + NSString *expMonthString = [NSString stringWithFormat:@"%02lu", (unsigned long)card.expMonth]; + STPCardValidationState expMonthValidation = [self validationStateForExpirationMonth:expMonthString]; + NSString *expYearString = [NSString stringWithFormat:@"%02lu", (unsigned long)card.expYear%100]; + STPCardValidationState expYearValidation = [self validationStateForExpirationYear:expYearString + inMonth:expMonthString + inCurrentYear:currentYear + currentMonth:currentMonth]; + STPCardBrand brand = [self brandForNumber:card.number]; + STPCardValidationState cvcValidation = [self validationStateForCVC:card.cvc cardBrand:brand]; + + NSArray *states = @[@(numberValidation), + @(expMonthValidation), + @(expYearValidation), + @(cvcValidation)]; + BOOL incomplete = NO; + for (NSNumber *boxedState in states) { + STPCardValidationState state = [boxedState integerValue]; + if (state == STPCardValidationStateInvalid) { + return state; + } + else if (state == STPCardValidationStateIncomplete) { + incomplete = YES; + } + } + return incomplete ? STPCardValidationStateIncomplete : STPCardValidationStateValid; +} + ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card { + return [self validationStateForCard:card + inCurrentYear:[self currentYear] + currentMonth:[self currentMonth]]; +} + ++ (NSUInteger)minCVCLength { + return 3; +} + ++ (NSUInteger)maxCVCLengthForCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: + case STPCardBrandUnknown: + return 4; + default: + return 3; + } +} + ++ (STPCardBrand)brandForNumber:(NSString *)cardNumber { + NSString *sanitizedNumber = [self sanitizedNumericStringForString:cardNumber]; + NSSet *brands = [self possibleBrandsForNumber:sanitizedNumber]; + if (brands.count == 1) { + return (STPCardBrand)[brands.anyObject integerValue]; + } + return STPCardBrandUnknown; +} + ++ (NSSet *)possibleBrandsForNumber:(NSString *)cardNumber { + NSArray *binRanges = [STPBINRange binRangesForNumber:cardNumber]; + NSMutableSet *possibleBrands = [NSMutableSet setWithArray:[binRanges valueForKeyPath:@"brand"]]; + [possibleBrands removeObject:@(STPCardBrandUnknown)]; + return [possibleBrands copy]; +} + ++ (NSSet*)lengthsForCardBrand:(STPCardBrand)brand { + NSMutableSet *set = [NSMutableSet set]; + NSArray *binRanges = [STPBINRange binRangesForBrand:brand]; + for (STPBINRange *binRange in binRanges) { + [set addObject:@(binRange.length)]; + } + return [set copy]; +} + ++ (NSInteger)lengthForCardBrand:(STPCardBrand)brand { + return [self maxLengthForCardBrand:brand]; +} + ++ (NSInteger)maxLengthForCardBrand:(STPCardBrand)brand { + NSInteger maxLength = -1; + for (NSNumber *length in [self lengthsForCardBrand:brand]) { + if (length.integerValue > maxLength) { + maxLength = length.integerValue; + } + } + return maxLength; +} + ++ (NSInteger)fragmentLengthForCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: + return 5; + case STPCardBrandDinersClub: + return 2; + default: + return 4; + } +} + ++ (BOOL)stringIsValidLuhn:(NSString *)number { + BOOL odd = true; + int sum = 0; + NSMutableArray *digits = [NSMutableArray arrayWithCapacity:number.length]; + + for (int i = 0; i < (NSInteger)number.length; i++) { + [digits addObject:[number substringWithRange:NSMakeRange(i, 1)]]; + } + + for (NSString *digitStr in [digits reverseObjectEnumerator]) { + int digit = [digitStr intValue]; + if ((odd = !odd)) digit *= 2; + if (digit > 9) digit -= 9; + sum += digit; + } + + return sum % 10 == 0; +} + ++ (NSInteger)currentYear { + NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *dateComponents = [calendar components:NSCalendarUnitYear fromDate:[NSDate date]]; + return dateComponents.year % 100; +} + ++ (NSInteger)currentMonth { + NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *dateComponents = [calendar components:NSCalendarUnitMonth fromDate:[NSDate date]]; + return dateComponents.month; +} + +@end diff --git a/TelegramUI/STPCustomer.h b/TelegramUI/STPCustomer.h new file mode 100755 index 0000000000..46c5a9f714 --- /dev/null +++ b/TelegramUI/STPCustomer.h @@ -0,0 +1,86 @@ +// +// STPCustomer.h +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import "STPSource.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An `STPCustomer` represents a deserialized Customer object from the Stripe API. You can use `STPCustomerDeserializer` to convert a JSON response from the Stripe API into an `STPCustomer`. + */ +@interface STPCustomer : NSObject + +/** + * Initialize a customer object with the provided values. + * + * @param stripeID The ID of the customer, e.g. `cus_abc` + * @param defaultSource The default source of the customer, such as an `STPCard` object. Can be nil. + * @param sources All of the customer's payment sources. This might be an empty array. + * + * @return an instance of STPCustomer + */ ++ (instancetype)customerWithStripeID:(NSString *)stripeID + defaultSource:(nullable id)defaultSource + sources:(NSArray> *)sources; + +/** + * The Stripe ID of the customer, e.g. `cus_1234` + */ +@property(nonatomic, readonly, copy)NSString *stripeID; + +/** + * The default source used to charge the customer. + */ +@property(nonatomic, readonly, nullable) id defaultSource; + +/** + * The available payment sources the customer has (this may be an empty array). + */ +@property(nonatomic, readonly) NSArray> *sources; + +@end + +/** + Use `STPCustomerDeserializer` to convert a response from the Stripe API into an `STPCustomer` object. `STPCustomerDeserializer` expects the JSON response to be in the exact same format as the Stripe API. + */ +@interface STPCustomerDeserializer : NSObject + +/** + * Initialize a customer deserializer. The `data`, `urlResponse`, and `error` parameters are intended to be passed from an `NSURLSessionDataTask` callback. After it has been initialized, you can inspect the `error` and `customer` properties to see if the deserialization was successful. If `error` is nil, `customer` will be non-nil (and vice versa). + * + * @param data An `NSData` object representing encoded JSON for a Customer object + * @param urlResponse The URL response obtained from the `NSURLSessionTask` + * @param error Any error that occurred from the URL session task (if this is non-nil, the `error` property will be set to this value after initialization). + * + */ +- (instancetype)initWithData:(nullable NSData *)data + urlResponse:(nullable NSURLResponse *)urlResponse + error:(nullable NSError *)error; + +/** + * Initializes a customer deserializer with a JSON dictionary. This JSON should be in the exact same format as what the Stripe API returns. If it's successfully parsed, the `customer` parameter will be present after initialization; otherwise `error` will be present. + * + * @param json a JSON dictionary. + * + */ +- (instancetype)initWithJSONResponse:(id)json; + +/** + * If a customer was successfully parsed from the response, it will be set here. Otherwise, this value wil be nil (and the `error` property will explain what went wrong). + */ +@property(nonatomic, readonly, nullable)STPCustomer *customer; + +/** + * If the deserializer failed to parse a customer, this property will explain why (and the `customer` property will be nil). + */ +@property(nonatomic, readonly, nullable)NSError *error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPCustomer.m b/TelegramUI/STPCustomer.m new file mode 100755 index 0000000000..ff3b7c3e6f --- /dev/null +++ b/TelegramUI/STPCustomer.m @@ -0,0 +1,101 @@ +// +// STPCustomer.m +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPCustomer.h" +#import "StripeError.h" +#import "STPCard.h" + +@interface STPCustomer() + +@property(nonatomic, copy)NSString *stripeID; +@property(nonatomic) id defaultSource; +@property(nonatomic) NSArray> *sources; + +@end + +@implementation STPCustomer + ++ (instancetype)customerWithStripeID:(NSString *)stripeID + defaultSource:(id)defaultSource + sources:(NSArray> *)sources { + STPCustomer *customer = [self new]; + customer.stripeID = stripeID; + customer.defaultSource = defaultSource; + customer.sources = sources; + return customer; +} + +@end + +@interface STPCustomerDeserializer() + +@property(nonatomic, nullable)STPCustomer *customer; +@property(nonatomic, nullable)NSError *error; + +@end + +@implementation STPCustomerDeserializer + +- (instancetype)initWithData:(nullable NSData *)data + urlResponse:(nullable __unused NSURLResponse *)urlResponse + error:(nullable NSError *)error { + if (error) { + return [self initWithError:error]; + } + NSError *jsonError; + id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (!json) { + return [self initWithError:jsonError]; + } + return [self initWithJSONResponse:json]; +} + +- (instancetype)initWithError:(NSError *)error { + self = [super init]; + if (self) { + _error = error; + } + return self; +} + +- (instancetype)initWithJSONResponse:(id)json { + self = [super init]; + if (self) { + if (![json isKindOfClass:[NSDictionary class]] || ![json[@"id"] isKindOfClass:[NSString class]]) { + _error = [NSError stp_genericFailedToParseResponseError]; + return self; + } + STPCustomer *customer = [STPCustomer new]; + customer.stripeID = json[@"id"]; + NSString *defaultSourceId; + if ([json[@"default_source"] isKindOfClass:[NSString class]]) { + defaultSourceId = json[@"default_source"]; + } + NSMutableArray *sources = [NSMutableArray array]; + if ([json[@"sources"] isKindOfClass:[NSDictionary class]] && [json[@"sources"][@"data"] isKindOfClass:[NSArray class]]) { + for (id contents in json[@"sources"][@"data"]) { + if ([contents isKindOfClass:[NSDictionary class]]) { + // eventually support other source types + STPCard *card = [STPCard decodedObjectFromAPIResponse:contents]; + // ignore apple pay cards from the response + if (card && !card.isApplePayCard) { + [sources addObject:card]; + if (defaultSourceId && [card.stripeID isEqualToString:defaultSourceId]) { + customer.defaultSource = card; + } + } + } + } + customer.sources = sources; + } + _customer = customer; + } + return self; +} + +@end diff --git a/TelegramUI/STPDelegateProxy.h b/TelegramUI/STPDelegateProxy.h new file mode 100755 index 0000000000..8fdc2fa835 --- /dev/null +++ b/TelegramUI/STPDelegateProxy.h @@ -0,0 +1,16 @@ +// +// STPDelegateProxy.h +// Stripe +// +// Created by Jack Flintermann on 10/20/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface STPDelegateProxy<__covariant DelegateType:NSObject *> : NSObject + +@property(nonatomic, weak)DelegateType delegate; +- (instancetype)init; + +@end diff --git a/TelegramUI/STPDelegateProxy.m b/TelegramUI/STPDelegateProxy.m new file mode 100755 index 0000000000..a36a931162 --- /dev/null +++ b/TelegramUI/STPDelegateProxy.m @@ -0,0 +1,41 @@ +// +// STPDelegateProxy.m +// Stripe +// +// Created by Jack Flintermann on 10/20/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPDelegateProxy.h" + +@implementation STPDelegateProxy + +- (instancetype)init { + return self; +} + +- (BOOL)respondsToSelector:(SEL)selector { + return [super respondsToSelector:selector] || [_delegate respondsToSelector:selector]; +} + +- (id)forwardingTargetForSelector:(SEL)selector { + if ([self respondsToSelector:selector]) { + return self; + } + if ([_delegate respondsToSelector:selector]) { + return _delegate; + } + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [super methodSignatureForSelector:selector] ?: [_delegate methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + if ([_delegate respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:_delegate]; + } +} + +@end diff --git a/TelegramUI/STPDispatchFunctions.h b/TelegramUI/STPDispatchFunctions.h new file mode 100755 index 0000000000..44d8778485 --- /dev/null +++ b/TelegramUI/STPDispatchFunctions.h @@ -0,0 +1,11 @@ +// +// STPDispatchFunctions.h +// Stripe +// +// Created by Brian Dorfman on 10/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#include + +void stpDispatchToMainThreadIfNecessary(dispatch_block_t block); diff --git a/TelegramUI/STPDispatchFunctions.m b/TelegramUI/STPDispatchFunctions.m new file mode 100755 index 0000000000..a64f30b18d --- /dev/null +++ b/TelegramUI/STPDispatchFunctions.m @@ -0,0 +1,18 @@ +// +// STPDispatchFunctions.m +// Stripe +// +// Created by Brian Dorfman on 10/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#include "STPDispatchFunctions.h" + +void stpDispatchToMainThreadIfNecessary(dispatch_block_t block) { + if ([NSThread isMainThread]) { + block(); + } + else { + dispatch_async(dispatch_get_main_queue(), block); + } +} diff --git a/TelegramUI/STPFormEncodable.h b/TelegramUI/STPFormEncodable.h new file mode 100755 index 0000000000..2ebe38ee5d --- /dev/null +++ b/TelegramUI/STPFormEncodable.h @@ -0,0 +1,35 @@ +// +// STPFormEncodable.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * Objects conforming to STPFormEncodable can be automatically converted to a form-encoded string, which can then be used when making requests to the Stripe API. + */ +@protocol STPFormEncodable + +/** + * The root object name to be used when converting this object to a form-encoded string. For example, if this returns @"card", then the form-encoded output will resemble @"card[foo]=bar" (where 'foo' and 'bar' are specified by `propertyNamesToFormFieldNamesMapping` below. + */ ++ (nonnull NSString *)rootObjectName; + +/** + * This maps properties on an object that is being form-encoded into parameter names in the Stripe API. For example, STPCardParams has a field called `expMonth`, but the Stripe API expects a field called `exp_month`. This dictionary represents a mapping from the former to the latter (in other words, [STPCardParams propertyNamesToFormFieldNamesMapping][@"expMonth"] == @"exp_month".) + */ ++ (nonnull NSDictionary *)propertyNamesToFormFieldNamesMapping; + +/** + * You can use this property to add additional fields to an API request that are not explicitly defined by the object's interface. This can be useful when using beta features that haven't been added to the Stripe SDK yet. For example, if the /v1/tokens API began to accept a beta field called "test_field", you might do the following: + STPCardParams *cardParams = [STPCardParams new]; + // add card values + cardParams.additionalAPIParameters = @{@"test_field": @"example_value"}; + [[STPAPIClient sharedClient] createTokenWithCard:cardParams completion:...]; + */ +@property(nonatomic, readwrite, nonnull, copy)NSDictionary *additionalAPIParameters; + +@end diff --git a/TelegramUI/STPFormEncoder.h b/TelegramUI/STPFormEncoder.h new file mode 100755 index 0000000000..1d4d2f089d --- /dev/null +++ b/TelegramUI/STPFormEncoder.h @@ -0,0 +1,24 @@ +// +// STPFormEncoder.h +// Stripe +// +// Created by Jack Flintermann on 1/8/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +@class STPCardParams, STPBankAccountParams; +@protocol STPFormEncodable; + +@interface STPFormEncoder : NSObject + ++ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject *)object; + ++ (nonnull NSString *)stringByURLEncoding:(nonnull NSString *)string; + ++ (nonnull NSString *)stringByReplacingSnakeCaseWithCamelCase:(nonnull NSString *)input; + ++ (nonnull NSString *)queryStringFromParameters:(nonnull NSDictionary *)parameters; + +@end diff --git a/TelegramUI/STPFormEncoder.m b/TelegramUI/STPFormEncoder.m new file mode 100755 index 0000000000..01b15851a9 --- /dev/null +++ b/TelegramUI/STPFormEncoder.m @@ -0,0 +1,188 @@ +// +// STPFormEncoder.m +// Stripe +// +// Created by Jack Flintermann on 1/8/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPFormEncoder.h" +#import "STPCardParams.h" + +FOUNDATION_EXPORT NSString * STPPercentEscapedStringFromString(NSString *string); +FOUNDATION_EXPORT NSString * STPQueryStringFromParameters(NSDictionary *parameters); + +@implementation STPFormEncoder + ++ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input { + NSArray *parts = [input componentsSeparatedByString:@"_"]; + NSMutableString *camelCaseParam = [NSMutableString string]; + [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { + [camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])]; + }]; + + return [camelCaseParam copy]; +} + ++ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject *)object { + NSDictionary *keyPairs = [self keyPairDictionaryForObject:object]; + NSString *rootObjectName = [object.class rootObjectName]; + NSDictionary *dict = rootObjectName != nil ? @{ rootObjectName: keyPairs } : keyPairs; + return [STPQueryStringFromParameters(dict) dataUsingEncoding:NSUTF8StringEncoding]; +} + ++ (NSDictionary *)keyPairDictionaryForObject:(nonnull NSObject *)object { + NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary]; + [[object.class propertyNamesToFormFieldNamesMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull formFieldName, __unused BOOL * _Nonnull stop) { + id value = [self formEncodableValueForObject:[object valueForKey:propertyName]]; + if (value) { + keyPairs[formFieldName] = value; + } + }]; + [object.additionalAPIParameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull additionalFieldName, id _Nonnull additionalFieldValue, __unused BOOL * _Nonnull stop) { + id value = [self formEncodableValueForObject:additionalFieldValue]; + if (value) { + keyPairs[additionalFieldName] = value; + } + }]; + return [keyPairs copy]; +} + ++ (id)formEncodableValueForObject:(NSObject *)object { + if ([object conformsToProtocol:@protocol(STPFormEncodable)]) { + return [self keyPairDictionaryForObject:(NSObject*)object]; + } else { + return object; + } +} + ++ (NSString *)stringByURLEncoding:(NSString *)string { + return STPPercentEscapedStringFromString(string); +} + ++ (NSString *)queryStringFromParameters:(NSDictionary *)parameters { + return STPQueryStringFromParameters(parameters); +} + +@end + + +// This code is adapted from https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFURLRequestSerialization.m . The only modifications are to replace the AF namespace with the STP namespace to avoid collisions with apps that are using both Stripe and AFNetworking. +NSString * STPPercentEscapedStringFromString(NSString *string) { + static NSString * const kSTPCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4 + static NSString * const kSTPCharactersSubDelimitersToEncode = @"!$&'()*+,;="; + + NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [allowedCharacterSet removeCharactersInString:[kSTPCharactersGeneralDelimitersToEncode stringByAppendingString:kSTPCharactersSubDelimitersToEncode]]; + + // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028 + // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + + static NSUInteger const batchSize = 50; + + NSUInteger index = 0; + NSMutableString *escaped = @"".mutableCopy; + + while (index < string.length) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wgnu" + NSUInteger length = MIN(string.length - index, batchSize); +#pragma GCC diagnostic pop + NSRange range = NSMakeRange(index, length); + + // To avoid breaking up character sequences such as 👴🏻👮🏽 + range = [string rangeOfComposedCharacterSequencesForRange:range]; + + NSString *substring = [string substringWithRange:range]; + NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + [escaped appendString:encoded]; + + index += range.length; + } + + return escaped; +} + +#pragma mark - + +@interface STPQueryStringPair : NSObject +@property (readwrite, nonatomic, strong) id field; +@property (readwrite, nonatomic, strong) id value; + +- (instancetype)initWithField:(id)field value:(id)value; + +- (NSString *)URLEncodedStringValue; +@end + +@implementation STPQueryStringPair + +- (instancetype)initWithField:(id)field value:(id)value { + self = [super init]; + if (!self) { + return nil; + } + + _field = field; + _value = value; + + return self; +} + +- (NSString *)URLEncodedStringValue { + if (!self.value || [self.value isEqual:[NSNull null]]) { + return STPPercentEscapedStringFromString([self.field description]); + } else { + return [NSString stringWithFormat:@"%@=%@", STPPercentEscapedStringFromString([self.field description]), STPPercentEscapedStringFromString([self.value description])]; + } +} + +@end + +#pragma mark - + +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary); +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value); + +NSString * STPQueryStringFromParameters(NSDictionary *parameters) { + NSMutableArray *mutablePairs = [NSMutableArray array]; + for (STPQueryStringPair *pair in STPQueryStringPairsFromDictionary(parameters)) { + [mutablePairs addObject:[pair URLEncodedStringValue]]; + } + + return [mutablePairs componentsJoinedByString:@"&"]; +} + +NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary) { + return STPQueryStringPairsFromKeyAndValue(nil, dictionary); +} + +NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value) { + NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; + NSString *descriptionSelector = NSStringFromSelector(@selector(description)); + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptionSelector ascending:YES selector:@selector(compare:)]; + + if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = value; + // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries + for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + id nestedValue = dictionary[nestedKey]; + if (nestedValue) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; + } + } + } else if ([value isKindOfClass:[NSArray class]]) { + NSArray *array = value; + for (id nestedValue in array) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; + } + } else if ([value isKindOfClass:[NSSet class]]) { + NSSet *set = value; + for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue(key, obj)]; + } + } else { + [mutableQueryStringComponents addObject:[[STPQueryStringPair alloc] initWithField:key value:value]]; + } + + return mutableQueryStringComponents; +} diff --git a/TelegramUI/STPFormTextField.h b/TelegramUI/STPFormTextField.h new file mode 100755 index 0000000000..218df2d1be --- /dev/null +++ b/TelegramUI/STPFormTextField.h @@ -0,0 +1,42 @@ +// +// STPFormTextField.h +// Stripe +// +// Created by Jack Flintermann on 7/16/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +@class STPFormTextField; + +typedef NS_ENUM(NSInteger, STPFormTextFieldAutoFormattingBehavior) { + STPFormTextFieldAutoFormattingBehaviorNone, + STPFormTextFieldAutoFormattingBehaviorPhoneNumbers, + STPFormTextFieldAutoFormattingBehaviorCardNumbers, + STPFormTextFieldAutoFormattingBehaviorExpiration, +}; + +@protocol STPFormTextFieldDelegate +@optional +- (void)formTextFieldDidBackspaceOnEmpty:(nonnull STPFormTextField *)formTextField; +- (nonnull NSAttributedString *)formTextField:(nonnull STPFormTextField *)formTextField + modifyIncomingTextChange:(nonnull NSAttributedString *)input; +- (void)formTextFieldTextDidChange:(nonnull STPFormTextField *)textField; +@end + +@interface STPFormTextField : UITextField + +@property(nonatomic, readwrite, nullable) UIColor *defaultColor; +@property(nonatomic, readwrite, nullable) UIColor *errorColor; +@property(nonatomic, readwrite, nullable) UIColor *placeholderColor; + +@property(nonatomic, readwrite, assign)BOOL selectionEnabled; // defaults to NO +@property(nonatomic, readwrite, assign)BOOL preservesContentsOnPaste; // defaults to NO +@property(nonatomic, readwrite, assign)STPFormTextFieldAutoFormattingBehavior autoFormattingBehavior; +@property(nonatomic, readwrite, assign)BOOL validText; +@property(nonatomic, readwrite, weak, nullable)idformDelegate; + +- (CGSize)measureTextSize; + +@end diff --git a/TelegramUI/STPFormTextField.m b/TelegramUI/STPFormTextField.m new file mode 100755 index 0000000000..153cbb4f8c --- /dev/null +++ b/TelegramUI/STPFormTextField.m @@ -0,0 +1,314 @@ +// +// STPFormTextField.m +// Stripe +// +// Created by Jack Flintermann on 7/24/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPFormTextField.h" +#import "STPCardValidator.h" +#import "STPPhoneNumberValidator.h" +#import "NSString+Stripe.h" +#import "STPDelegateProxy.h" +#import "STPWeakStrongMacros.h" + +#define FAUXPAS_IGNORED_IN_METHOD(...) + +@interface STPTextFieldDelegateProxy : STPDelegateProxy +@property(nonatomic, assign)STPFormTextFieldAutoFormattingBehavior autoformattingBehavior; +@property(nonatomic, assign)BOOL selectionEnabled; +@end + +@implementation STPTextFieldDelegateProxy + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + BOOL deleting = (range.location == textField.text.length - 1 && range.length == 1 && [string isEqualToString:@""]); + NSString *inputText; + if (deleting) { + NSString *sanitized = [self unformattedStringForString:textField.text]; + inputText = [sanitized stp_safeSubstringToIndex:sanitized.length - 1]; + } else { + NSString *newString = [textField.text stringByReplacingCharactersInRange:range withString:string]; + NSString *sanitized = [self unformattedStringForString:newString]; + inputText = sanitized; + } + + UITextPosition *beginning = textField.beginningOfDocument; + UITextPosition *start = [textField positionFromPosition:beginning offset:range.location]; + + if ([textField.text isEqualToString:inputText]) { + return NO; + } + + textField.text = inputText; + + if (self.autoformattingBehavior == STPFormTextFieldAutoFormattingBehaviorNone && self.selectionEnabled) { + + // this will be the new cursor location after insert/paste/typing + NSInteger cursorOffset = [textField offsetFromPosition:beginning toPosition:start] + string.length; + + UITextPosition *newCursorPosition = [textField positionFromPosition:textField.beginningOfDocument offset:cursorOffset]; + UITextRange *newSelectedRange = [textField textRangeFromPosition:newCursorPosition toPosition:newCursorPosition]; + [textField setSelectedTextRange:newSelectedRange]; + } + + return NO; +} + +- (NSString *)unformattedStringForString:(NSString *)string { + switch (self.autoformattingBehavior) { + case STPFormTextFieldAutoFormattingBehaviorNone: + return string; + case STPFormTextFieldAutoFormattingBehaviorCardNumbers: + case STPFormTextFieldAutoFormattingBehaviorPhoneNumbers: + case STPFormTextFieldAutoFormattingBehaviorExpiration: + return [STPCardValidator sanitizedNumericStringForString:string]; + } +} + +@end + +typedef NSAttributedString* (^STPFormTextTransformationBlock)(NSAttributedString *inputText); + +@interface STPFormTextField() +@property(nonatomic)STPTextFieldDelegateProxy *delegateProxy; +@property(nonatomic, copy)STPFormTextTransformationBlock textFormattingBlock; +// This property only exists to disable keyboard loading in Travis CI due to a crash that occurs while trying to load the keyboard. Don't use it outside of tests. +@property(nonatomic)BOOL skipsReloadingInputViews; +@end + +@implementation STPFormTextField + +@synthesize placeholderColor = _placeholderColor; + +- (void)reloadInputViews { + if (self.skipsReloadingInputViews) { + return; + } + [super reloadInputViews]; +} + ++ (NSDictionary *)attributesForAttributedString:(NSAttributedString *)attributedString { + if (attributedString.length == 0) { + return @{}; + } + return [attributedString attributesAtIndex:0 longestEffectiveRange:nil inRange:NSMakeRange(0, attributedString.length)]; +} + +- (void)setSelectionEnabled:(BOOL)selectionEnabled { + _selectionEnabled = selectionEnabled; + self.delegateProxy.selectionEnabled = selectionEnabled; +} + +- (void)setAutoFormattingBehavior:(STPFormTextFieldAutoFormattingBehavior)autoFormattingBehavior { + _autoFormattingBehavior = autoFormattingBehavior; + self.delegateProxy.autoformattingBehavior = autoFormattingBehavior; + switch (autoFormattingBehavior) { + case STPFormTextFieldAutoFormattingBehaviorNone: + case STPFormTextFieldAutoFormattingBehaviorExpiration: + self.textFormattingBlock = nil; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if ([self respondsToSelector:@selector(setTextContentType:)]) { + self.textContentType = nil; + } +#endif + break; + case STPFormTextFieldAutoFormattingBehaviorCardNumbers: + self.textFormattingBlock = ^NSAttributedString *(NSAttributedString *inputString) { + if (![STPCardValidator stringIsNumeric:inputString.string]) { + return [inputString copy]; + } + NSMutableAttributedString *attributedString = [inputString mutableCopy]; + NSArray *cardSpacing; + STPCardBrand currentBrand = [STPCardValidator brandForNumber:attributedString.string]; + if (currentBrand == STPCardBrandAmex) { + cardSpacing = @[@3, @9]; + } else { + cardSpacing = @[@3, @7, @11]; + } + for (NSUInteger i = 0; i < attributedString.length; i++) { + if ([cardSpacing containsObject:@(i)]) { + [attributedString addAttribute:NSKernAttributeName value:@(5) + range:NSMakeRange(i, 1)]; + } else { + [attributedString addAttribute:NSKernAttributeName value:@(0) + range:NSMakeRange(i, 1)]; + } + } + return [attributedString copy]; + }; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if ([self respondsToSelector:@selector(setTextContentType:)]) { + self.textContentType = UITextContentTypeCreditCardNumber; + } +#endif + break; + case STPFormTextFieldAutoFormattingBehaviorPhoneNumbers: { + WEAK(self); + self.textFormattingBlock = ^NSAttributedString *(NSAttributedString *inputString) { + if (![STPCardValidator stringIsNumeric:inputString.string]) { + return [inputString copy]; + } + STRONG(self); + NSString *phoneNumber = [STPPhoneNumberValidator formattedSanitizedPhoneNumberForString:inputString.string]; + NSDictionary *attributes = [[self class] attributesForAttributedString:inputString]; + return [[NSAttributedString alloc] initWithString:phoneNumber attributes:attributes]; + }; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if ([self respondsToSelector:@selector(setTextContentType:)]) { + self.textContentType = UITextContentTypeTelephoneNumber; + } +#endif + break; + } + } +} + +- (void)setFormDelegate:(id)formDelegate { + _formDelegate = formDelegate; + self.delegate = formDelegate; +} + +- (void)insertText:(NSString *)text { + [self setText:[self.text stringByAppendingString:text]]; +} + +- (void)deleteBackward { + [super deleteBackward]; + if (self.text.length == 0) { + if ([self.formDelegate respondsToSelector:@selector(formTextFieldDidBackspaceOnEmpty:)]) { + [self.formDelegate formTextFieldDidBackspaceOnEmpty:self]; + } + } +} + +- (CGSize)measureTextSize { + return self.attributedText.size; +} + +- (void)setText:(NSString *)text { + NSString *nonNilText = text ?: @""; + NSAttributedString *attributed = [[NSAttributedString alloc] initWithString:nonNilText attributes:self.defaultTextAttributes]; + [self setAttributedText:attributed]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + NSAttributedString *oldValue = [self attributedText]; + BOOL shouldModify = self.formDelegate && [self.formDelegate respondsToSelector:@selector(formTextField:modifyIncomingTextChange:)]; + NSAttributedString *modified = shouldModify ? + [self.formDelegate formTextField:self modifyIncomingTextChange:attributedText] : + attributedText; + NSAttributedString *transformed = self.textFormattingBlock ? self.textFormattingBlock(modified) : modified; + [super setAttributedText:transformed]; + [self sendActionsForControlEvents:UIControlEventEditingChanged]; + if ([self.formDelegate respondsToSelector:@selector(formTextFieldTextDidChange:)]) { + if (![transformed isEqualToAttributedString:oldValue]) { + [self.formDelegate formTextFieldTextDidChange:self]; + } + } +} + +- (void)setPlaceholder:(NSString *)placeholder { + NSString *nonNilPlaceholder = placeholder ?: @""; + NSAttributedString *attributedPlaceholder = [[NSAttributedString alloc] initWithString:nonNilPlaceholder attributes:[self placeholderTextAttributes]]; + [self setAttributedPlaceholder:attributedPlaceholder]; +} + +- (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder { + NSAttributedString *transformed = self.textFormattingBlock ? self.textFormattingBlock(attributedPlaceholder) : attributedPlaceholder; + [super setAttributedPlaceholder:transformed]; +} + +- (NSDictionary *)placeholderTextAttributes { + NSMutableDictionary *defaultAttributes = [[self defaultTextAttributes] mutableCopy]; + if (self.placeholderColor) { + defaultAttributes[NSForegroundColorAttributeName] = self.placeholderColor; + } + return [defaultAttributes copy]; +} + +- (void)setDefaultColor:(UIColor *)defaultColor { + _defaultColor = defaultColor; + [self updateColor]; +} + +- (void)setErrorColor:(UIColor *)errorColor { + _errorColor = errorColor; + [self updateColor]; +} + +- (void)setValidText:(BOOL)validText { + _validText = validText; + [self updateColor]; +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor { + _placeholderColor = placeholderColor; + [self setPlaceholder:self.placeholder]; //explicitly rebuild attributed placeholder +} + +- (void)updateColor { + self.textColor = _validText ? self.defaultColor : self.errorColor; +} + +// Workaround for http://www.openradar.appspot.com/19374610 +- (CGRect)editingRectForBounds:(CGRect)bounds { + if (UIDevice.currentDevice.systemVersion.integerValue != 8) { + return [self textRectForBounds:bounds]; + } + + CGFloat const scale = UIScreen.mainScreen.scale; + CGFloat const preferred = self.attributedText.size.height; + CGFloat const delta = (CGFloat)ceil(preferred) - preferred; + CGFloat const adjustment = (CGFloat)floor(delta * scale) / scale; + + CGRect const textRect = [self textRectForBounds:bounds]; + CGRect const editingRect = CGRectOffset(textRect, 0.0, adjustment); + + return editingRect; +} + +// Fixes a weird issue related to our custom override of deleteBackwards. This only affects the simulator and iPads with custom keyboards. +- (NSArray *)keyCommands { + FAUXPAS_IGNORED_IN_METHOD(APIAvailability); + return @[[UIKeyCommand keyCommandWithInput:@"\b" modifierFlags:UIKeyModifierCommand action:@selector(commandDeleteBackwards)]]; +} + +- (void)commandDeleteBackwards { + self.text = @""; +} + +- (UITextPosition *)closestPositionToPoint:(CGPoint)point { + if (self.selectionEnabled) { + return [super closestPositionToPoint:point]; + } + return [self positionFromPosition:self.beginningOfDocument offset:self.text.length]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + return [super canPerformAction:action withSender:sender] && action == @selector(paste:); +} + +- (void)paste:(id)sender { + if (self.preservesContentsOnPaste) { + [super paste:sender]; + } else { + self.text = [UIPasteboard generalPasteboard].string; + } +} + +- (void)setDelegate:(id )delegate { + STPTextFieldDelegateProxy *delegateProxy = [[STPTextFieldDelegateProxy alloc] init]; + delegateProxy.autoformattingBehavior = self.autoFormattingBehavior; + delegateProxy.selectionEnabled = self.selectionEnabled; + delegateProxy.delegate = delegate; + self.delegateProxy = delegateProxy; + [super setDelegate:delegateProxy]; +} + +- (id )delegate { + return self.delegateProxy; +} + +@end diff --git a/TelegramUI/STPImageLibrary+Private.h b/TelegramUI/STPImageLibrary+Private.h new file mode 100755 index 0000000000..b0e6b69e67 --- /dev/null +++ b/TelegramUI/STPImageLibrary+Private.h @@ -0,0 +1,35 @@ +// +// STPImageLibrary+Private.h +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPImageLibrary.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPImageLibrary (Private) + ++ (UIImage *)addIcon; ++ (UIImage *)leftChevronIcon; ++ (UIImage *)smallRightChevronIcon; ++ (UIImage *)checkmarkIcon; ++ (UIImage *)largeCardFrontImage; ++ (UIImage *)largeCardBackImage; ++ (UIImage *)largeCardApplePayImage; + ++ (UIImage *)safeImageNamed:(NSString *)imageName + templateIfAvailable:(BOOL)templateIfAvailable; ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)brand + template:(BOOL)isTemplate; ++ (UIImage *)imageWithTintColor:(UIColor *)color + forImage:(UIImage *)image; ++ (UIImage *)paddedImageWithInsets:(UIEdgeInsets)insets + forImage:(UIImage *)image; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPImageLibrary.h b/TelegramUI/STPImageLibrary.h new file mode 100755 index 0000000000..7b773a4123 --- /dev/null +++ b/TelegramUI/STPImageLibrary.h @@ -0,0 +1,79 @@ +// +// STPImages.h +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +#import "STPCardBrand.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class lets you access card icons used by the Stripe SDK. All icons are 32 x 20 points. + */ +@interface STPImageLibrary : NSObject + +/** + * An icon representing Apple Pay. + */ ++ (UIImage *)applePayCardImage; + +/** + * An icon representing American Express. + */ ++ (UIImage *)amexCardImage; + +/** + * An icon representing Diners Club. + */ ++ (UIImage *)dinersClubCardImage; + +/** + * An icon representing Discover. + */ ++ (UIImage *)discoverCardImage; + +/** + * An icon representing JCB. + */ ++ (UIImage *)jcbCardImage; + +/** + * An icon representing MasterCard. + */ ++ (UIImage *)masterCardCardImage; + +/** + * An icon representing Visa. + */ ++ (UIImage *)visaCardImage; + +/** + * An icon to use when the type of the card is unknown. + */ ++ (UIImage *)unknownCardCardImage; + +/** + * This returns the appropriate icon for the specified card brand. + */ ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)brand; + +/** + * This returns the appropriate icon for the specified card brand as a + * single color template that can be tinted + */ ++ (UIImage *)templatedBrandImageForCardBrand:(STPCardBrand)brand; + +/** + * This returns a small icon indicating the CVC location for the given card brand. + */ ++ (UIImage *)cvcImageForCardBrand:(STPCardBrand)brand; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPImageLibrary.m b/TelegramUI/STPImageLibrary.m new file mode 100755 index 0000000000..b1c0d889cd --- /dev/null +++ b/TelegramUI/STPImageLibrary.m @@ -0,0 +1,175 @@ +// +// STPImages.m +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPImageLibrary.h" +#import "STPImageLibrary+Private.h" + +#define FAUXPAS_IGNORED_IN_METHOD(...) + +// Dummy class for locating the framework bundle + + +@implementation STPImageLibrary + ++ (UIImage *)applePayCardImage { + return [self safeImageNamed:@"stp_card_applepay"]; +} + ++ (UIImage *)amexCardImage { + return [self brandImageForCardBrand:STPCardBrandAmex]; +} + ++ (UIImage *)dinersClubCardImage { + return [self brandImageForCardBrand:STPCardBrandDinersClub]; +} + ++ (UIImage *)discoverCardImage { + return [self brandImageForCardBrand:STPCardBrandDiscover]; +} + ++ (UIImage *)jcbCardImage { + return [self brandImageForCardBrand:STPCardBrandJCB]; +} + ++ (UIImage *)masterCardCardImage { + return [self brandImageForCardBrand:STPCardBrandMasterCard]; +} + ++ (UIImage *)visaCardImage { + return [self brandImageForCardBrand:STPCardBrandVisa]; +} + ++ (UIImage *)unknownCardCardImage { + return [self brandImageForCardBrand:STPCardBrandUnknown]; +} + ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)brand { + return [self brandImageForCardBrand:brand template:NO]; +} + ++ (UIImage *)templatedBrandImageForCardBrand:(STPCardBrand)brand { + return [self brandImageForCardBrand:brand template:YES]; +} + ++ (UIImage *)cvcImageForCardBrand:(STPCardBrand)brand { + NSString *imageName = brand == STPCardBrandAmex ? @"stp_card_cvc_amex" : @"stp_card_cvc"; + return [self safeImageNamed:imageName]; +} + ++ (UIImage *)safeImageNamed:(NSString *)imageName { + return [self safeImageNamed:imageName templateIfAvailable:NO]; +} + +@end + +@implementation STPImageLibrary (Private) + ++ (UIImage *)addIcon { + return [self safeImageNamed:@"stp_icon_add" templateIfAvailable:YES]; +} + ++ (UIImage *)leftChevronIcon { + return [self safeImageNamed:@"stp_icon_chevron_left" templateIfAvailable:YES]; +} + ++ (UIImage *)smallRightChevronIcon { + return [self safeImageNamed:@"stp_icon_chevron_right_small" templateIfAvailable:YES]; +} + ++ (UIImage *)checkmarkIcon { + return [self safeImageNamed:@"stp_icon_checkmark" templateIfAvailable:YES]; +} + ++ (UIImage *)largeCardFrontImage { + return [self safeImageNamed:@"stp_card_form_front" templateIfAvailable:YES]; +} + ++ (UIImage *)largeCardBackImage { + return [self safeImageNamed:@"stp_card_form_back" templateIfAvailable:YES]; +} + ++ (UIImage *)largeCardApplePayImage { + return [self safeImageNamed:@"stp_card_form_applepay" templateIfAvailable:YES]; +} + ++ (UIImage *)safeImageNamed:(NSString *)imageName + templateIfAvailable:(BOOL)templateIfAvailable { + FAUXPAS_IGNORED_IN_METHOD(APIAvailability); + UIImage *image = nil; + if ([UIImage respondsToSelector:@selector(imageNamed:inBundle:compatibleWithTraitCollection:)]) { + image = [UIImage imageNamed:imageName inBundle:[NSBundle bundleForClass:[STPImageLibrary class]] compatibleWithTraitCollection:nil]; + } + if (image == nil) { + image = [UIImage imageNamed:imageName]; + } + if (templateIfAvailable) { + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + return image; +} + ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)brand + template:(BOOL)isTemplate { + BOOL shouldUseTemplate = isTemplate; + FAUXPAS_IGNORED_IN_METHOD(APIAvailability); + NSString *imageName; + switch (brand) { + case STPCardBrandAmex: + imageName = shouldUseTemplate ? @"stp_card_amex_template" : @"stp_card_amex"; + break; + case STPCardBrandDinersClub: + imageName = shouldUseTemplate ? @"stp_card_diners_template" : @"stp_card_diners"; + break; + case STPCardBrandDiscover: + imageName = shouldUseTemplate ? @"stp_card_discover_template" : @"stp_card_discover"; + break; + case STPCardBrandJCB: + imageName = shouldUseTemplate ? @"stp_card_jcb_template" : @"stp_card_jcb"; + break; + case STPCardBrandMasterCard: + imageName = shouldUseTemplate ? @"stp_card_mastercard_template" : @"stp_card_mastercard"; + break; + case STPCardBrandUnknown: + shouldUseTemplate = YES; + imageName = @"stp_card_placeholder_template"; + break; + case STPCardBrandVisa: + imageName = shouldUseTemplate ? @"stp_card_visa_template" : @"stp_card_visa"; + break; + } + UIImage *image = [self safeImageNamed:imageName + templateIfAvailable:shouldUseTemplate]; + return image; +} + ++ (UIImage *)imageWithTintColor:(UIColor *)color + forImage:(UIImage *)image { + UIImage *newImage; + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + [color set]; + UIImage *templateImage = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [templateImage drawInRect:CGRectMake(0, 0, templateImage.size.width, templateImage.size.height)]; + newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; +} + ++ (UIImage *)paddedImageWithInsets:(UIEdgeInsets)insets + forImage:(UIImage *)image { + CGSize size = CGSizeMake(image.size.width + insets.left + insets.right, + image.size.height + insets.top + insets.bottom); + UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); + CGPoint origin = CGPointMake(insets.left, insets.top); + [image drawAtPoint:origin]; + UIImage *imageWithInsets = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + imageWithInsets = [imageWithInsets imageWithRenderingMode:image.renderingMode]; + return imageWithInsets; +} + +@end diff --git a/TelegramUI/STPPaymentCardTextField.h b/TelegramUI/STPPaymentCardTextField.h new file mode 100755 index 0000000000..e64b20b230 --- /dev/null +++ b/TelegramUI/STPPaymentCardTextField.h @@ -0,0 +1,298 @@ +// +// STPPaymentCardTextField.h +// Stripe +// +// Created by Jack Flintermann on 7/16/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +#import "STPCard.h" + +@class STPPaymentCardTextField; + +/** + * This protocol allows a delegate to be notified when a payment text field's contents change, which can in turn be used to take further actions depending on the validity of its contents. + */ +@protocol STPPaymentCardTextFieldDelegate +@optional +/** + * Called when either the card number, expiration, or CVC changes. At this point, one can call -isValid on the text field to determine, for example, whether or not to enable a button to submit the form. Example: + + - (void)paymentCardTextFieldDidChange:(STPPaymentCardTextField *)textField { + self.paymentButton.enabled = textField.isValid; + } + + * + * @param textField the text field that has changed + */ +- (void)paymentCardTextFieldDidChange:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing begins in the payment card field's number field. + */ +- (void)paymentCardTextFieldDidBeginEditingNumber:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing ends in the payment card field's number field. + */ +- (void)paymentCardTextFieldDidEndEditingNumber:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing begins in the payment card field's CVC field. + */ +- (void)paymentCardTextFieldDidBeginEditingCVC:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing ends in the payment card field's CVC field. + */ +- (void)paymentCardTextFieldDidEndEditingCVC:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing begins in the payment card field's expiration field. + */ +- (void)paymentCardTextFieldDidBeginEditingExpiration:(nonnull STPPaymentCardTextField *)textField; + +/** + * Called when editing ends in the payment card field's expiration field. + */ +- (void)paymentCardTextFieldDidEndEditingExpiration:(nonnull STPPaymentCardTextField *)textField; + +@end + + +/** + * STPPaymentCardTextField is a text field with similar properties to UITextField, but specialized for collecting credit/debit card information. It manages multiple UITextFields under the hood to collect this information. It's designed to fit on a single line, and from a design perspective can be used anywhere a UITextField would be appropriate. + */ +@interface STPPaymentCardTextField : UIControl + +/** + * @see STPPaymentCardTextFieldDelegate + */ +@property(nonatomic, weak, nullable) IBOutlet id delegate; + +/** + * The font used in each child field. Default is [UIFont systemFontOfSize:18]. Set this property to nil to reset to the default. + */ +@property(nonatomic, copy, null_resettable) UIFont *font UI_APPEARANCE_SELECTOR; + +/** + * The text color to be used when entering valid text. Default is [UIColor blackColor]. Set this property to nil to reset to the default. + */ +@property(nonatomic, copy, null_resettable) UIColor *textColor UI_APPEARANCE_SELECTOR; + +/** + * The text color to be used when the user has entered invalid information, such as an invalid card number. Default is [UIColor redColor]. Set this property to nil to reset to the default. + */ +@property(nonatomic, copy, null_resettable) UIColor *textErrorColor UI_APPEARANCE_SELECTOR; + +/** + * The text placeholder color used in each child field. Default is [UIColor lightGreyColor]. Set this property to nil to reset to the default. On iOS 7 and above, this will also set the color of the card placeholder icon. + */ +@property(nonatomic, copy, null_resettable) UIColor *placeholderColor UI_APPEARANCE_SELECTOR; + +/** + * The placeholder for the card number field. Default is @"1234567812345678". If this is set to something that resembles a card number, it will automatically format it as such (in other words, you don't need to add spaces to this string). + */ +@property(nonatomic, copy, nullable) NSString *numberPlaceholder; + +/** + * The placeholder for the expiration field. Defaults to @"MM/YY". + */ +@property(nonatomic, copy, nullable) NSString *expirationPlaceholder; + +/** + * The placeholder for the cvc field. Defaults to @"CVC". + */ +@property(nonatomic, copy, nullable) NSString *cvcPlaceholder; + +/** + * The cursor color for the field. This is a proxy for the view's tintColor property, exposed for clarity only (in other words, calling setCursorColor is identical to calling setTintColor). + */ +@property(nonatomic, copy, null_resettable) UIColor *cursorColor UI_APPEARANCE_SELECTOR; + +/** + * The border color for the field. Default is [UIColor lightGreyColor]. Can be nil (in which case no border will be drawn). + */ +@property(nonatomic, copy, nullable) UIColor *borderColor UI_APPEARANCE_SELECTOR; + +/** + * The width of the field's border. Default is 1.0. + */ +@property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR; + +/** + * The corner radius for the field's border. Default is 5.0. + */ +@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; + +/** + * The keyboard appearance for the field. Default is UIKeyboardAppearanceDefault. + */ +@property(nonatomic, assign) UIKeyboardAppearance keyboardAppearance UI_APPEARANCE_SELECTOR; + +/** + * This behaves identically to setting the inputAccessoryView for each child text field. + */ +@property(nonatomic, strong, nullable) UIView *inputAccessoryView; + +/** + * The curent brand image displayed in the receiver. + */ +@property (nonatomic, readonly, nullable) UIImage *brandImage; + +/** + * Causes the text field to begin editing. Presents the keyboard. + * + * @return Whether or not the text field successfully began editing. + * @see UIResponder + */ +- (BOOL)becomeFirstResponder; + +/** + * Causes the text field to stop editing. Dismisses the keyboard. + * + * @return Whether or not the field successfully stopped editing. + * @see UIResponder + */ +- (BOOL)resignFirstResponder; + +/** + * Resets all of the contents of all of the fields. If the field is currently being edited, the number field will become selected. + */ +- (void)clear; + +/** + * Returns the cvc image used for a card brand. + * Override this method in a subclass if you would like to provide custom images. + * @param cardBrand The brand of card entered. + * @return The cvc image used for a card brand. + */ ++ (nullable UIImage *)cvcImageForCardBrand:(STPCardBrand)cardBrand; + +/** + * Returns the brand image used for a card brand. + * Override this method in a subclass if you would like to provide custom images. + * @param cardBrand The brand of card entered. + * @return The brand image used for a card brand. + */ ++ (nullable UIImage *)brandImageForCardBrand:(STPCardBrand)cardBrand; + +/** + * Returns the rectangle in which the receiver draws its brand image. + * @param bounds The bounding rectangle of the receiver. + * @return the rectangle in which the receiver draws its brand image. + */ +- (CGRect)brandImageRectForBounds:(CGRect)bounds; + +/** + * Returns the rectangle in which the receiver draws the text fields. + * @param bounds The bounding rectangle of the receiver. + * @return The rectangle in which the receiver draws the text fields. + */ +- (CGRect)fieldsRectForBounds:(CGRect)bounds; + +/** + * Returns the rectangle in which the receiver draws its number field. + * @param bounds The bounding rectangle of the receiver. + * @return the rectangle in which the receiver draws its number field. + */ +- (CGRect)numberFieldRectForBounds:(CGRect)bounds; + +/** + * Returns the rectangle in which the receiver draws its cvc field. + * @param bounds The bounding rectangle of the receiver. + * @return the rectangle in which the receiver draws its cvc field. + */ +- (CGRect)cvcFieldRectForBounds:(CGRect)bounds; + +/** + * Returns the rectangle in which the receiver draws its expiration field. + * @param bounds The bounding rectangle of the receiver. + * @return the rectangle in which the receiver draws its expiration field. + */ +- (CGRect)expirationFieldRectForBounds:(CGRect)bounds; + +/** + * Whether or not the form currently contains a valid card number, expiration date, and CVC. + * @see STPCardValidator + */ +@property(nonatomic, readonly)BOOL isValid; +@property(nonatomic, readonly)BOOL valid; // for backwards-compatibility + +/** + * Enable/disable selecting or editing the field. Useful when submitting card details to Stripe. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * The current card number displayed by the field. May or may not be valid, unless isValid is true, in which case it is guaranteed to be valid. + */ +@property(nonatomic, readonly, nullable) NSString *cardNumber; + +/** + * The current expiration month displayed by the field (1 = January, etc). May or may not be valid, unless isValid is true, in which case it is guaranteed to be valid. + */ +@property(nonatomic, readonly) NSUInteger expirationMonth; + +/** + * The current expiration month displayed by the field, as a string. This may or may not be a valid entry (i.e. "0", and may be 0-prefixed (i.e. "01" for January). You can use [STPCardValidator validationStateForExpirationMonth] to validate this value. + */ +@property(nonatomic, readonly, nullable) NSString *formattedExpirationMonth; + +/** + * The current expiration year displayed by the field, modulo 100 (e.g. the year 2015 will be represented as 15). May or may not be valid, unless isValid is true, in which case it is guaranteed to be valid. + */ +@property(nonatomic, readonly) NSUInteger expirationYear; + +/** + * The current expiration year displayed by the field, as a string. This is a 2-digit year (i.e. "15"), and may or may not be a valid entry. You can use [STPCardValidator validationStateForExpirationYear:inMonth] to validate this value. + */ +@property(nonatomic, readonly, nullable) NSString *formattedExpirationYear; + +/** + * The current card CVC displayed by the field. May or may not be valid, unless isValid is true, in which case it is guaranteed to be valid. + */ +@property(nonatomic, readonly, nullable) NSString *cvc; + +/** + * Convenience property for creating an STPCardParams from the currently entered information + * or programmatically setting the field's contents. For example, if you're using another library + * to scan your user's credit card with a camera, you can assemble that data into an STPCardParams + * object and set this property to that object to prefill the fields you've collected. + */ +@property(nonatomic, strong, readwrite, nonnull) STPCardParams *cardParams; + +@property(nonatomic, strong, readwrite, nullable) STPCardParams *card __attribute__((deprecated("This has been renamed to cardParams; use that instead."))); + +- (void)commonInit; + +@end + +#pragma mark - PaymentKit compatibility + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + +__attribute__((deprecated("This class is provided only for backwards-compatibility with PaymentKit. You shouldn't use it - use STPCard instead."))) +@interface PTKCard : STPCard +@end + +@class PTKView; + +__attribute__((deprecated("This protocol is provided only for backwards-compatibility with PaymentKit. You shouldn't use it - use STPPaymentCardTextFieldDelegate instead."))) +@protocol PTKViewDelegate + +@optional +- (void)paymentView:(nonnull PTKView *)paymentView withCard:(nonnull PTKCard *)card isValid:(BOOL)valid; + +@end + +__attribute__((deprecated("This class is provided only for backwards-compatibility with PaymentKit. You shouldn't use it - use STPPaymentCardTextField instead."))) +@interface PTKView : STPPaymentCardTextField +@property(nonatomic, weak, nullable)iddelegate; +@property(nonatomic, strong, readwrite, nonnull) PTKCard *card; +@end + +#pragma clang diagnostic pop diff --git a/TelegramUI/STPPaymentCardTextField.m b/TelegramUI/STPPaymentCardTextField.m new file mode 100755 index 0000000000..6da4512595 --- /dev/null +++ b/TelegramUI/STPPaymentCardTextField.m @@ -0,0 +1,845 @@ +// +// STPPaymentCardTextField.m +// Stripe +// +// Created by Jack Flintermann on 7/16/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +//#import "Stripe.h" +#import "STPPaymentCardTextField.h" +#import "STPPaymentCardTextFieldViewModel.h" +#import "STPFormTextField.h" +#import "STPImageLibrary.h" +#import "STPWeakStrongMacros.h" + +#define FAUXPAS_IGNORED_IN_METHOD(...) + +@interface STPPaymentCardTextField() + +@property(nonatomic, readwrite, strong)STPFormTextField *sizingField; + +@property(nonatomic, readwrite, weak)UIImageView *brandImageView; +@property(nonatomic, readwrite, weak)UIView *fieldsView; + +@property(nonatomic, readwrite, weak)STPFormTextField *numberField; + +@property(nonatomic, readwrite, weak)STPFormTextField *expirationField; + +@property(nonatomic, readwrite, weak)STPFormTextField *cvcField; + +@property(nonatomic, readwrite, strong)STPPaymentCardTextFieldViewModel *viewModel; + +@property(nonatomic, readonly, weak)UITextField *currentFirstResponderField; + +@property(nonatomic, assign)BOOL numberFieldShrunk; + +@property(nonatomic, readwrite, strong)STPCardParams *internalCardParams; + +@end + +@implementation STPPaymentCardTextField + +@synthesize font = _font; +@synthesize textColor = _textColor; +@synthesize textErrorColor = _textErrorColor; +@synthesize placeholderColor = _placeholderColor; +@synthesize borderColor = _borderColor; +@synthesize borderWidth = _borderWidth; +@synthesize cornerRadius = _cornerRadius; +@dynamic enabled; + +CGFloat const STPPaymentCardTextFieldDefaultPadding = 13; + +#if CGFLOAT_IS_DOUBLE +#define stp_roundCGFloat(x) round(x) +#else +#define stp_roundCGFloat(x) roundf(x) +#endif + +#pragma mark initializers + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self commonInit]; + } + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self commonInit]; + } + return self; +} + +- (void)commonInit { + // We're using ivars here because UIAppearance tracks when setters are + // called, and won't override properties that have already been customized + _borderColor = [self.class placeholderGrayColor]; + _cornerRadius = 5.0f; + _borderWidth = 1.0f; + self.layer.borderColor = [[_borderColor copy] CGColor]; + self.layer.cornerRadius = _cornerRadius; + self.layer.borderWidth = _borderWidth; + + self.clipsToBounds = YES; + + _internalCardParams = [STPCardParams new]; + _viewModel = [STPPaymentCardTextFieldViewModel new]; + _sizingField = [self buildTextField]; + _sizingField.formDelegate = nil; + + UIImageView *brandImageView = [[UIImageView alloc] initWithImage:self.brandImage]; + brandImageView.contentMode = UIViewContentModeCenter; + brandImageView.backgroundColor = [UIColor clearColor]; + if ([brandImageView respondsToSelector:@selector(setTintColor:)]) { + brandImageView.tintColor = self.placeholderColor; + } + self.brandImageView = brandImageView; + + STPFormTextField *numberField = [self buildTextField]; + numberField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorCardNumbers; + numberField.tag = STPCardFieldTypeNumber; + numberField.accessibilityLabel = NSLocalizedString(@"card number", @"accessibility label for text field"); + self.numberField = numberField; + self.numberPlaceholder = [self.viewModel defaultPlaceholder]; + + STPFormTextField *expirationField = [self buildTextField]; + expirationField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorExpiration; + expirationField.tag = STPCardFieldTypeExpiration; + expirationField.alpha = 0; + expirationField.accessibilityLabel = NSLocalizedString(@"expiration date", @"accessibility label for text field"); + self.expirationField = expirationField; + self.expirationPlaceholder = @"MM/YY"; + + STPFormTextField *cvcField = [self buildTextField]; + cvcField.tag = STPCardFieldTypeCVC; + cvcField.alpha = 0; + self.cvcField = cvcField; + self.cvcPlaceholder = @"CVC"; + self.cvcField.accessibilityLabel = self.cvcPlaceholder; + + UIView *fieldsView = [[UIView alloc] init]; + fieldsView.clipsToBounds = YES; + fieldsView.backgroundColor = [UIColor clearColor]; + self.fieldsView = fieldsView; + + [self addSubview:self.fieldsView]; + [self.fieldsView addSubview:cvcField]; + [self.fieldsView addSubview:expirationField]; + [self.fieldsView addSubview:numberField]; + [self addSubview:brandImageView]; +} + +- (STPPaymentCardTextFieldViewModel *)viewModel { + if (_viewModel == nil) { + _viewModel = [STPPaymentCardTextFieldViewModel new]; + } + return _viewModel; +} + +#pragma mark appearance properties + ++ (UIColor *)placeholderGrayColor { + return [UIColor lightGrayColor]; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + [super setBackgroundColor:[backgroundColor copy]]; + self.numberField.backgroundColor = self.backgroundColor; +} + +- (UIColor *)backgroundColor { + return [super backgroundColor] ?: [UIColor whiteColor]; +} + +- (void)setFont:(UIFont *)font { + _font = [font copy]; + + for (UITextField *field in [self allFields]) { + field.font = _font; + } + + self.sizingField.font = _font; + + [self setNeedsLayout]; +} + +- (UIFont *)font { + return _font ?: [UIFont systemFontOfSize:18]; +} + +- (void)setTextColor:(UIColor *)textColor { + _textColor = [textColor copy]; + + for (STPFormTextField *field in [self allFields]) { + field.defaultColor = _textColor; + } +} + +- (void)setContentVerticalAlignment:(UIControlContentVerticalAlignment)contentVerticalAlignment { + [super setContentVerticalAlignment:contentVerticalAlignment]; + for (UITextField *field in [self allFields]) { + field.contentVerticalAlignment = contentVerticalAlignment; + } + switch (contentVerticalAlignment) { + case UIControlContentVerticalAlignmentCenter: + self.brandImageView.contentMode = UIViewContentModeCenter; + break; + case UIControlContentVerticalAlignmentBottom: + self.brandImageView.contentMode = UIViewContentModeBottom; + break; + case UIControlContentVerticalAlignmentFill: + self.brandImageView.contentMode = UIViewContentModeTop; + break; + case UIControlContentVerticalAlignmentTop: + self.brandImageView.contentMode = UIViewContentModeTop; + break; + } +} + +- (UIColor *)textColor { + return _textColor ?: [UIColor blackColor]; +} + +- (void)setTextErrorColor:(UIColor *)textErrorColor { + _textErrorColor = [textErrorColor copy]; + + for (STPFormTextField *field in [self allFields]) { + field.errorColor = _textErrorColor; + } +} + +- (UIColor *)textErrorColor { + return _textErrorColor ?: [UIColor redColor]; +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor { + _placeholderColor = [placeholderColor copy]; + + if ([self.brandImageView respondsToSelector:@selector(setTintColor:)]) { + self.brandImageView.tintColor = placeholderColor; + } + + for (STPFormTextField *field in [self allFields]) { + field.placeholderColor = _placeholderColor; + } +} + +- (UIColor *)placeholderColor { + return _placeholderColor ?: [self.class placeholderGrayColor]; +} + +- (void)setNumberPlaceholder:(NSString * __nullable)numberPlaceholder { + _numberPlaceholder = [numberPlaceholder copy]; + self.numberField.placeholder = _numberPlaceholder; +} + +- (void)setExpirationPlaceholder:(NSString * __nullable)expirationPlaceholder { + _expirationPlaceholder = [expirationPlaceholder copy]; + self.expirationField.placeholder = _expirationPlaceholder; +} + +- (void)setCvcPlaceholder:(NSString * __nullable)cvcPlaceholder { + _cvcPlaceholder = [cvcPlaceholder copy]; + self.cvcField.placeholder = _cvcPlaceholder; +} + +- (void)setCursorColor:(UIColor *)cursorColor { + self.tintColor = cursorColor; +} + +- (UIColor *)cursorColor { + return self.tintColor; +} + +- (void)setBorderColor:(UIColor * __nullable)borderColor { + _borderColor = borderColor; + if (borderColor) { + self.layer.borderColor = [[borderColor copy] CGColor]; + } + else { + self.layer.borderColor = [[UIColor clearColor] CGColor]; + } +} + +- (UIColor * __nullable)borderColor { + return _borderColor; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + self.layer.cornerRadius = cornerRadius; +} + +- (CGFloat)cornerRadius { + return _cornerRadius; +} + +- (void)setBorderWidth:(CGFloat)borderWidth { + _borderWidth = borderWidth; + self.layer.borderWidth = borderWidth; +} + +- (CGFloat)borderWidth { + return _borderWidth; +} + +- (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance { + _keyboardAppearance = keyboardAppearance; + for (STPFormTextField *field in [self allFields]) { + field.keyboardAppearance = keyboardAppearance; + } +} + +- (void)setInputAccessoryView:(UIView *)inputAccessoryView { + _inputAccessoryView = inputAccessoryView; + + for (STPFormTextField *field in [self allFields]) { + field.inputAccessoryView = inputAccessoryView; + } +} + +#pragma mark UIControl + +- (void)setEnabled:(BOOL)enabled { + [super setEnabled:enabled]; + for (STPFormTextField *textField in [self allFields]) { + textField.enabled = enabled; + }; +} + +#pragma mark UIResponder & related methods + +- (BOOL)isFirstResponder { + return [self.currentFirstResponderField isFirstResponder]; +} + +- (BOOL)canBecomeFirstResponder { + return [[self nextFirstResponderField] canBecomeFirstResponder]; +} + +- (BOOL)becomeFirstResponder { + return [[self nextFirstResponderField] becomeFirstResponder]; +} + +- (STPFormTextField *)nextFirstResponderField { + if ([self.viewModel validationStateForField:STPCardFieldTypeNumber] != STPCardValidationStateValid) { + return self.numberField; + } else if ([self.viewModel validationStateForField:STPCardFieldTypeExpiration] != STPCardValidationStateValid) { + return self.expirationField; + } else { + return self.cvcField; + } +} + +- (STPFormTextField *)currentFirstResponderField { + for (STPFormTextField *textField in [self allFields]) { + if ([textField isFirstResponder]) { + return textField; + } + } + return nil; +} + +- (BOOL)canResignFirstResponder { + return [self.currentFirstResponderField canResignFirstResponder]; +} + +- (BOOL)resignFirstResponder { + [super resignFirstResponder]; + BOOL success = [self.currentFirstResponderField resignFirstResponder]; + [self setNumberFieldShrunk:[self shouldShrinkNumberField] animated:YES completion:nil]; + [self updateImageForFieldType:STPCardFieldTypeNumber]; + return success; +} + +- (STPFormTextField *)previousField { + if (self.currentFirstResponderField == self.cvcField) { + return self.expirationField; + } else if (self.currentFirstResponderField == self.expirationField) { + return self.numberField; + } + return nil; +} + +#pragma mark public convenience methods + +- (void)clear { + for (STPFormTextField *field in [self allFields]) { + field.text = @""; + } + self.viewModel = [STPPaymentCardTextFieldViewModel new]; + [self onChange]; + [self updateImageForFieldType:STPCardFieldTypeNumber]; + WEAK(self); + [self setNumberFieldShrunk:NO animated:YES completion:^(__unused BOOL completed){ + STRONG(self); + if ([self isFirstResponder]) { + [[self numberField] becomeFirstResponder]; + } + }]; +} + +- (BOOL)isValid { + return [self.viewModel isValid]; +} + +- (BOOL)valid { + return self.isValid; +} + +#pragma mark readonly variables + +- (NSString *)cardNumber { + return self.viewModel.cardNumber; +} + +- (NSUInteger)expirationMonth { + return [self.viewModel.expirationMonth integerValue]; +} + +- (NSUInteger)expirationYear { + return [self.viewModel.expirationYear integerValue]; +} + +- (NSString *)formattedExpirationMonth { + return self.viewModel.expirationMonth; +} + +- (NSString *)formattedExpirationYear { + return self.viewModel.expirationYear; +} + +- (NSString *)cvc { + return self.viewModel.cvc; +} + +- (STPCardParams *)cardParams { + self.internalCardParams.number = self.cardNumber; + self.internalCardParams.expMonth = self.expirationMonth; + self.internalCardParams.expYear = self.expirationYear; + self.internalCardParams.cvc = self.cvc; + return self.internalCardParams; +} + +- (void)setCardParams:(STPCardParams *)cardParams { + self.internalCardParams = cardParams; + [self setText:cardParams.number inField:STPCardFieldTypeNumber]; + BOOL expirationPresent = cardParams.expMonth && cardParams.expYear; + if (expirationPresent) { + NSString *text = [NSString stringWithFormat:@"%02lu%02lu", + (unsigned long)cardParams.expMonth, + (unsigned long)cardParams.expYear%100]; + [self setText:text inField:STPCardFieldTypeExpiration]; + } + else { + [self setText:@"" inField:STPCardFieldTypeExpiration]; + } + [self setText:cardParams.cvc inField:STPCardFieldTypeCVC]; + + BOOL shrinkNumberField = [self shouldShrinkNumberField]; + [self setNumberFieldShrunk:shrinkNumberField animated:NO completion:nil]; + if ([self isFirstResponder]) { + [[self nextFirstResponderField] becomeFirstResponder]; + } + + // update the card image, falling back to the number field image if not editing + if ([self.expirationField isFirstResponder]) { + [self updateImageForFieldType:STPCardFieldTypeExpiration]; + } + else if ([self.cvcField isFirstResponder]) { + [self updateImageForFieldType:STPCardFieldTypeCVC]; + } + else { + [self updateImageForFieldType:STPCardFieldTypeNumber]; + } +} + +- (STPCardParams *)card { + if (!self.isValid) { return nil; } + return self.cardParams; +} + +- (void)setCard:(STPCardParams *)card { + [self setCardParams:card]; +} + +- (void)setText:(NSString *)text inField:(STPCardFieldType)field { + NSString *nonNilText = text ?: @""; + STPFormTextField *textField = nil; + switch (field) { + case STPCardFieldTypeNumber: + textField = self.numberField; + break; + case STPCardFieldTypeExpiration: + textField = self.expirationField; + break; + case STPCardFieldTypeCVC: + textField = self.cvcField; + break; + } + textField.text = nonNilText; +} + +- (CGSize)intrinsicContentSize { + + CGSize imageSize = self.brandImage.size; + + self.sizingField.text = self.viewModel.defaultPlaceholder; + CGFloat textHeight = [self.sizingField measureTextSize].height; + CGFloat imageHeight = imageSize.height + (STPPaymentCardTextFieldDefaultPadding); + CGFloat height = stp_roundCGFloat((MAX(MAX(imageHeight, textHeight), 44))); + + CGFloat width = stp_roundCGFloat([self widthForCardNumber:self.viewModel.defaultPlaceholder] + imageSize.width + (STPPaymentCardTextFieldDefaultPadding * 3)); + + return CGSizeMake(width, height); +} + +- (CGRect)brandImageRectForBounds:(CGRect)bounds { + return CGRectMake(STPPaymentCardTextFieldDefaultPadding, 0, self.brandImageView.image.size.width, bounds.size.height - 1); +} + +- (CGRect)fieldsRectForBounds:(CGRect)bounds { + CGRect brandImageRect = [self brandImageRectForBounds:bounds]; + return CGRectMake(CGRectGetMaxX(brandImageRect), 0, CGRectGetWidth(bounds) - CGRectGetMaxX(brandImageRect), CGRectGetHeight(bounds)); +} + +- (CGRect)numberFieldRectForBounds:(CGRect)bounds { + CGFloat placeholderWidth = [self widthForCardNumber:self.numberField.placeholder] - 4; + CGFloat numberWidth = [self widthForCardNumber:self.viewModel.defaultPlaceholder] - 4; + CGFloat numberFieldWidth = MAX(placeholderWidth, numberWidth); + CGFloat nonFragmentWidth = [self widthForCardNumber:[self.viewModel numberWithoutLastDigits]] - 12; + CGFloat numberFieldX = self.numberFieldShrunk ? STPPaymentCardTextFieldDefaultPadding - nonFragmentWidth : 8; + return CGRectMake(numberFieldX, 0, numberFieldWidth, CGRectGetHeight(bounds)); +} + +- (CGRect)cvcFieldRectForBounds:(CGRect)bounds { + CGRect fieldsRect = [self fieldsRectForBounds:bounds]; + + CGFloat cvcWidth = MAX([self widthForText:self.cvcField.placeholder], [self widthForText:@"8888"]); + CGFloat cvcX = self.numberFieldShrunk ? + CGRectGetWidth(fieldsRect) - cvcWidth - STPPaymentCardTextFieldDefaultPadding / 2 : + CGRectGetWidth(fieldsRect); + return CGRectMake(cvcX, 0, cvcWidth, CGRectGetHeight(bounds)); +} + +- (CGRect)expirationFieldRectForBounds:(CGRect)bounds { + CGRect numberFieldRect = [self numberFieldRectForBounds:bounds]; + CGRect cvcRect = [self cvcFieldRectForBounds:bounds]; + + CGFloat expirationWidth = MAX([self widthForText:self.expirationField.placeholder], [self widthForText:@"88/88"]); + CGFloat expirationX = (CGRectGetMaxX(numberFieldRect) + CGRectGetMinX(cvcRect) - expirationWidth) / 2; + return CGRectMake(expirationX, 0, expirationWidth, CGRectGetHeight(bounds)); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGRect bounds = self.bounds; + + self.brandImageView.frame = [self brandImageRectForBounds:bounds]; + self.fieldsView.frame = [self fieldsRectForBounds:bounds]; + self.numberField.frame = [self numberFieldRectForBounds:bounds]; + self.cvcField.frame = [self cvcFieldRectForBounds:bounds]; + self.expirationField.frame = [self expirationFieldRectForBounds:bounds]; + +} + +#pragma mark - private helper methods + +- (STPFormTextField *)buildTextField { + STPFormTextField *textField = [[STPFormTextField alloc] initWithFrame:CGRectZero]; + textField.backgroundColor = [UIColor clearColor]; + textField.keyboardType = UIKeyboardTypePhonePad; + textField.font = self.font; + textField.defaultColor = self.textColor; + textField.errorColor = self.textErrorColor; + textField.placeholderColor = self.placeholderColor; + textField.formDelegate = self; + textField.validText = true; + return textField; +} + +- (NSArray *)allFields { + NSMutableArray *mutable = [NSMutableArray array]; + if (self.numberField) { + [mutable addObject:self.numberField]; + } + if (self.expirationField) { + [mutable addObject:self.expirationField]; + } + if (self.cvcField) { + [mutable addObject:self.cvcField]; + } + return [mutable copy]; +} + +typedef void (^STPNumberShrunkCompletionBlock)(BOOL completed); +- (void)setNumberFieldShrunk:(BOOL)shrunk animated:(BOOL)animated + completion:(STPNumberShrunkCompletionBlock)completion { + + if (_numberFieldShrunk == shrunk) { + if (completion) { + completion(YES); + } + return; + } + + _numberFieldShrunk = shrunk; + void (^animations)() = ^void() { + for (UIView *view in @[self.expirationField, self.cvcField]) { + view.alpha = 1.0f * shrunk; + } + [self layoutSubviews]; + }; + + FAUXPAS_IGNORED_IN_METHOD(APIAvailability); + NSTimeInterval duration = animated * 0.3; + if ([UIView respondsToSelector:@selector(animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)]) { + [UIView animateWithDuration:duration + delay:0 + usingSpringWithDamping:0.85f + initialSpringVelocity:0 + options:0 + animations:animations + completion:completion]; + } else { + [UIView animateWithDuration:duration + animations:animations + completion:completion]; + } +} + +- (BOOL)shouldShrinkNumberField { + return [self.viewModel validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateValid; +} + +- (CGFloat)widthForText:(NSString *)text { + self.sizingField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorNone; + [self.sizingField setText:text]; + return [self.sizingField measureTextSize].width + 8; +} + +- (CGFloat)widthForCardNumber:(NSString *)cardNumber { + self.sizingField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorCardNumbers; + [self.sizingField setText:cardNumber]; + return [self.sizingField measureTextSize].width + 20; +} + +#pragma mark STPFormTextFieldDelegate + +- (void)formTextFieldDidBackspaceOnEmpty:(__unused STPFormTextField *)formTextField { + STPFormTextField *previous = [self previousField]; + [previous becomeFirstResponder]; + [previous deleteBackward]; +} + +- (NSAttributedString *)formTextField:(STPFormTextField *)formTextField + modifyIncomingTextChange:(NSAttributedString *)input { + STPCardFieldType fieldType = formTextField.tag; + switch (fieldType) { + case STPCardFieldTypeNumber: + self.viewModel.cardNumber = input.string; + break; + case STPCardFieldTypeExpiration: { + self.viewModel.rawExpiration = input.string; + break; + } + case STPCardFieldTypeCVC: + self.viewModel.cvc = input.string; + break; + } + + switch (fieldType) { + case STPCardFieldTypeNumber: + return [[NSAttributedString alloc] initWithString:self.viewModel.cardNumber + attributes:self.numberField.defaultTextAttributes]; + case STPCardFieldTypeExpiration: + return [[NSAttributedString alloc] initWithString:self.viewModel.rawExpiration + attributes:self.expirationField.defaultTextAttributes]; + case STPCardFieldTypeCVC: + return [[NSAttributedString alloc] initWithString:self.viewModel.cvc + attributes:self.cvcField.defaultTextAttributes]; + } +} + +- (void)formTextFieldTextDidChange:(STPFormTextField *)formTextField { + STPCardFieldType fieldType = formTextField.tag; + if (fieldType == STPCardFieldTypeNumber) { + [self updateImageForFieldType:fieldType]; + } + + STPCardValidationState state = [self.viewModel validationStateForField:fieldType]; + formTextField.validText = YES; + switch (state) { + case STPCardValidationStateInvalid: + formTextField.validText = NO; + break; + case STPCardValidationStateIncomplete: + break; + case STPCardValidationStateValid: { + [[self nextFirstResponderField] becomeFirstResponder]; + break; + } + } + + [self onChange]; +} + +- (void)textFieldDidBeginEditing:(UITextField *)textField { + switch ((STPCardFieldType)textField.tag) { + case STPCardFieldTypeNumber: + [self setNumberFieldShrunk:NO animated:YES completion:nil]; + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingNumber:)]) { + [self.delegate paymentCardTextFieldDidBeginEditingNumber:self]; + } + break; + case STPCardFieldTypeCVC: + [self setNumberFieldShrunk:YES animated:YES completion:nil]; + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingCVC:)]) { + [self.delegate paymentCardTextFieldDidBeginEditingCVC:self]; + } + break; + case STPCardFieldTypeExpiration: + [self setNumberFieldShrunk:YES animated:YES completion:nil]; + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingExpiration:)]) { + [self.delegate paymentCardTextFieldDidBeginEditingExpiration:self]; + } + break; + } + [self updateImageForFieldType:textField.tag]; +} + +- (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField { + [self updateImageForFieldType:STPCardFieldTypeNumber]; + return YES; +} + +- (void)textFieldDidEndEditing:(UITextField *)textField { + switch ((STPCardFieldType)textField.tag) { + case STPCardFieldTypeNumber: + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingNumber:)]) { + [self.delegate paymentCardTextFieldDidEndEditingNumber:self]; + } + break; + case STPCardFieldTypeCVC: + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingCVC:)]) { + [self.delegate paymentCardTextFieldDidEndEditingCVC:self]; + } + break; + case STPCardFieldTypeExpiration: + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingExpiration:)]) { + [self.delegate paymentCardTextFieldDidEndEditingExpiration:self]; + } + break; + } +} + +- (UIImage *)brandImage { + if (self.currentFirstResponderField) { + return [self brandImageForFieldType:self.currentFirstResponderField.tag]; + } else { + return [self brandImageForFieldType:STPCardFieldTypeNumber]; + } +} + ++ (UIImage *)cvcImageForCardBrand:(STPCardBrand)cardBrand { + return [STPImageLibrary cvcImageForCardBrand:cardBrand]; +} + ++ (UIImage *)brandImageForCardBrand:(STPCardBrand)cardBrand { + return [STPImageLibrary brandImageForCardBrand:cardBrand]; +} + +- (UIImage *)brandImageForFieldType:(STPCardFieldType)fieldType { + if (fieldType == STPCardFieldTypeCVC) { + return [self.class cvcImageForCardBrand:self.viewModel.brand]; + } + + return [self.class brandImageForCardBrand:self.viewModel.brand]; +} + +- (void)updateImageForFieldType:(STPCardFieldType)fieldType { + UIImage *image = [self brandImageForFieldType:fieldType]; + if (image != self.brandImageView.image) { + self.brandImageView.image = image; + + CATransition *transition = [CATransition animation]; + transition.duration = 0.2f; + transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + transition.type = kCATransitionFade; + + [self.brandImageView.layer addAnimation:transition forKey:nil]; + + [self setNeedsLayout]; + } +} + +- (void)onChange { + if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidChange:)]) { + [self.delegate paymentCardTextFieldDidChange:self]; + } + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +#pragma mark UIKeyInput + +- (BOOL)hasText { + return self.numberField.hasText || self.expirationField.hasText || self.cvcField.hasText; +} + +- (void)insertText:(NSString *)text { + [self.currentFirstResponderField insertText:text]; +} + +- (void)deleteBackward { + [self.currentFirstResponderField deleteBackward]; +} + +@end + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation PTKCard +@end + +@interface PTKView() +@property(nonatomic, weak)idinternalDelegate; +@end + +@implementation PTKView + +@dynamic delegate, card; + +- (void)setDelegate:(id __nullable)delegate { + self.internalDelegate = delegate; +} + +- (id __nullable)delegate { + return self.internalDelegate; +} + +- (void)onChange { + [super onChange]; + [self.internalDelegate paymentView:self withCard:[self card] isValid:self.isValid]; +} + +- (PTKCard * __nonnull)card { + PTKCard *card = [[PTKCard alloc] init]; + card.number = self.cardNumber; + card.expMonth = self.expirationMonth; + card.expYear = self.expirationYear; + card.cvc = self.cvc; + return card; +} + +@end + +#pragma clang diagnostic pop diff --git a/TelegramUI/STPPaymentCardTextFieldViewModel.h b/TelegramUI/STPPaymentCardTextFieldViewModel.h new file mode 100755 index 0000000000..3917a6e089 --- /dev/null +++ b/TelegramUI/STPPaymentCardTextFieldViewModel.h @@ -0,0 +1,37 @@ +// +// STPPaymentCardTextFieldViewModel.h +// Stripe +// +// Created by Jack Flintermann on 7/21/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import +#import + +#import "STPCard.h" +#import "STPCardValidator.h" + +typedef NS_ENUM(NSInteger, STPCardFieldType) { + STPCardFieldTypeNumber, + STPCardFieldTypeExpiration, + STPCardFieldTypeCVC, +}; + +@interface STPPaymentCardTextFieldViewModel : NSObject + +@property(nonatomic, readwrite, copy, nullable)NSString *cardNumber; +@property(nonatomic, readwrite, copy, nullable)NSString *rawExpiration; +@property(nonatomic, readonly, nullable)NSString *expirationMonth; +@property(nonatomic, readonly, nullable)NSString *expirationYear; +@property(nonatomic, readwrite, copy, nullable)NSString *cvc; +@property(nonatomic, readonly) STPCardBrand brand; + +- (nonnull NSString *)defaultPlaceholder; +- (nullable NSString *)numberWithoutLastDigits; + +- (BOOL)isValid; + +- (STPCardValidationState)validationStateForField:(STPCardFieldType)fieldType; + +@end diff --git a/TelegramUI/STPPaymentCardTextFieldViewModel.m b/TelegramUI/STPPaymentCardTextFieldViewModel.m new file mode 100755 index 0000000000..014691eba7 --- /dev/null +++ b/TelegramUI/STPPaymentCardTextFieldViewModel.m @@ -0,0 +1,105 @@ +// +// STPPaymentCardTextFieldViewModel.m +// Stripe +// +// Created by Jack Flintermann on 7/21/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentCardTextFieldViewModel.h" +#import "NSString+Stripe.h" + +#define FAUXPAS_IGNORED_IN_METHOD(...) + +@implementation STPPaymentCardTextFieldViewModel + +- (void)setCardNumber:(NSString *)cardNumber { + NSString *sanitizedNumber = [STPCardValidator sanitizedNumericStringForString:cardNumber]; + STPCardBrand brand = [STPCardValidator brandForNumber:sanitizedNumber]; + NSInteger maxLength = [STPCardValidator maxLengthForCardBrand:brand]; + _cardNumber = [sanitizedNumber stp_safeSubstringToIndex:maxLength]; +} + +// This might contain slashes. +- (void)setRawExpiration:(NSString *)expiration { + NSString *sanitizedExpiration = [STPCardValidator sanitizedNumericStringForString:expiration]; + self.expirationMonth = [sanitizedExpiration stp_safeSubstringToIndex:2]; + self.expirationYear = [[sanitizedExpiration stp_safeSubstringFromIndex:2] stp_safeSubstringToIndex:2]; +} + +- (NSString *)rawExpiration { + NSMutableArray *array = [@[] mutableCopy]; + if (self.expirationMonth && ![self.expirationMonth isEqualToString:@""]) { + [array addObject:self.expirationMonth]; + } + + if ([STPCardValidator validationStateForExpirationMonth:self.expirationMonth] == STPCardValidationStateValid) { + [array addObject:self.expirationYear]; + } + return [array componentsJoinedByString:@"/"]; +} + +- (void)setExpirationMonth:(NSString *)expirationMonth { + NSString *sanitizedExpiration = [STPCardValidator sanitizedNumericStringForString:expirationMonth]; + if (sanitizedExpiration.length == 1 && ![sanitizedExpiration isEqualToString:@"0"] && ![sanitizedExpiration isEqualToString:@"1"]) { + sanitizedExpiration = [@"0" stringByAppendingString:sanitizedExpiration]; + } + _expirationMonth = [sanitizedExpiration stp_safeSubstringToIndex:2]; +} + +- (void)setExpirationYear:(NSString *)expirationYear { + _expirationYear = [[STPCardValidator sanitizedNumericStringForString:expirationYear] stp_safeSubstringToIndex:2]; +} + +- (void)setCvc:(NSString *)cvc { + NSInteger maxLength = [STPCardValidator maxCVCLengthForCardBrand:self.brand]; + _cvc = [[STPCardValidator sanitizedNumericStringForString:cvc] stp_safeSubstringToIndex:maxLength]; +} + +- (STPCardBrand)brand { + return [STPCardValidator brandForNumber:self.cardNumber]; +} + +- (STPCardValidationState)validationStateForField:(STPCardFieldType)fieldType { + switch (fieldType) { + case STPCardFieldTypeNumber: + return [STPCardValidator validationStateForNumber:self.cardNumber validatingCardBrand:YES]; + break; + case STPCardFieldTypeExpiration: { + STPCardValidationState monthState = [STPCardValidator validationStateForExpirationMonth:self.expirationMonth]; + STPCardValidationState yearState = [STPCardValidator validationStateForExpirationYear:self.expirationYear inMonth:self.expirationMonth]; + if (monthState == STPCardValidationStateValid && yearState == STPCardValidationStateValid) { + return STPCardValidationStateValid; + } else if (monthState == STPCardValidationStateInvalid || yearState == STPCardValidationStateInvalid) { + return STPCardValidationStateInvalid; + } else { + return STPCardValidationStateIncomplete; + } + break; + } + case STPCardFieldTypeCVC: + return [STPCardValidator validationStateForCVC:self.cvc cardBrand:self.brand]; + } +} + +- (BOOL)isValid { + return ([self validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateValid && + [self validationStateForField:STPCardFieldTypeExpiration] == STPCardValidationStateValid && + [self validationStateForField:STPCardFieldTypeCVC] == STPCardValidationStateValid); +} + +- (NSString *)defaultPlaceholder { + return @"1234567812345678"; +} + +- (NSString *)numberWithoutLastDigits { + NSUInteger length = [STPCardValidator fragmentLengthForCardBrand:[STPCardValidator brandForNumber:self.cardNumber]]; + NSUInteger toIndex = self.cardNumber.length - length; + + return (toIndex < self.cardNumber.length) ? + [self.cardNumber substringToIndex:toIndex] : + [self.defaultPlaceholder stp_safeSubstringToIndex:[self defaultPlaceholder].length - length]; + +} + +@end diff --git a/TelegramUI/STPPaymentConfiguration+Private.h b/TelegramUI/STPPaymentConfiguration+Private.h new file mode 100755 index 0000000000..2ced368774 --- /dev/null +++ b/TelegramUI/STPPaymentConfiguration+Private.h @@ -0,0 +1,16 @@ +// +// STPPaymentConfiguration+Private.h +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentConfiguration.h" + +@interface STPPaymentConfiguration (Private) + +@property(nonatomic, readonly)BOOL applePayEnabled; + +@end + diff --git a/TelegramUI/STPPaymentConfiguration.h b/TelegramUI/STPPaymentConfiguration.h new file mode 100755 index 0000000000..05f24962b4 --- /dev/null +++ b/TelegramUI/STPPaymentConfiguration.h @@ -0,0 +1,65 @@ +// +// STPPaymentConfiguration.h +// Stripe +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import "STPBackendAPIAdapter.h" +#import "STPPaymentMethod.h" + +NS_ASSUME_NONNULL_BEGIN + + +/** + An `STPPaymentConfiguration` represents all the options you can set or change + around a payment. + + You provide an `STPPaymentConfiguration` object to your `STPPaymentContext` + when making a charge. The configuration generally has settings that + will not change from payment to payment and thus is reusable, while the context + is specific to a single particular payment instance. + */ +@interface STPPaymentConfiguration : NSObject + +/** + This is a convenience singleton configuration that uses the default values + for every property + */ ++ (instancetype)sharedConfiguration; + +/** + * Your Stripe publishable key. You can get this from https://dashboard.stripe.com/account/apikeys . + */ +@property(nonatomic, copy)NSString *publishableKey; + +/** + * An enum value representing which payment methods you will accept from your user in addition to credit cards. Unless you have a very specific reason not to, you should leave this at the default, `STPPaymentMethodTypeAll`. + */ +@property(nonatomic)STPPaymentMethodType additionalPaymentMethods; + +/** + * The billing address fields the user must fill out when prompted for their payment details. These fields will all be present on the returned token from Stripe. See https://stripe.com/docs/api#create_card_token for more information. + */ +@property(nonatomic)STPBillingAddressFields requiredBillingAddressFields; + +/** + * The name of your company, for displaying to the user during payment flows. For example, when using Apple Pay, the payment sheet's final line item will read "PAY {companyName}". This defaults to the name of your iOS application. + */ +@property(nonatomic, copy)NSString *companyName; + +/** + * The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay. + */ +@property(nonatomic, nullable, copy)NSString *appleMerchantIdentifier; + +/** + * When entering their payment information, users who have a saved card with Stripe will be prompted to autofill it by entering an SMS code. Set this property to `YES` to disable this feature. The user won't receive an SMS code even if they have their payment information stored with Stripe, and won't be prompted to save it if they don't. + */ +@property(nonatomic)BOOL smsAutofillDisabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPPaymentConfiguration.m b/TelegramUI/STPPaymentConfiguration.m new file mode 100755 index 0000000000..794b9beaed --- /dev/null +++ b/TelegramUI/STPPaymentConfiguration.m @@ -0,0 +1,58 @@ +// +// STPPaymentConfiguration.m +// Stripe +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentConfiguration.h" +#import "STPPaymentConfiguration+Private.h" +#import "STPAPIClient.h" +#import "STPAPIClient+ApplePay.h" + +@implementation STPPaymentConfiguration + ++ (instancetype)sharedConfiguration { + static STPPaymentConfiguration *sharedConfiguration; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedConfiguration = [self new]; + }); + return sharedConfiguration; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalPaymentMethods = STPPaymentMethodTypeAll; + _requiredBillingAddressFields = STPBillingAddressFieldsNone; + _companyName = @"Telegram"; + _smsAutofillDisabled = NO; + } + return self; +} + +- (id)copyWithZone:(__unused NSZone *)zone { + STPPaymentConfiguration *copy = [self.class new]; + copy.publishableKey = self.publishableKey; + copy.additionalPaymentMethods = self.additionalPaymentMethods; + copy.requiredBillingAddressFields = self.requiredBillingAddressFields; + copy.companyName = self.companyName; + copy.appleMerchantIdentifier = self.appleMerchantIdentifier; + copy.smsAutofillDisabled = self.smsAutofillDisabled; + return copy; +} + +@end + +@implementation STPPaymentConfiguration (Private) + +- (BOOL)applePayEnabled { + return self.appleMerchantIdentifier && + (self.additionalPaymentMethods & STPPaymentMethodTypeApplePay) && + [Stripe deviceSupportsApplePay]; +} + +@end + diff --git a/TelegramUI/STPPaymentMethod.h b/TelegramUI/STPPaymentMethod.h new file mode 100755 index 0000000000..08908a269e --- /dev/null +++ b/TelegramUI/STPPaymentMethod.h @@ -0,0 +1,51 @@ +// +// STPPaymentMethod.h +// Stripe +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +/** + * This represents all of the payment methods available to your user (in addition to card payments, which are always enabled) when configuring an `STPPaymentContext`. + */ +typedef NS_OPTIONS(NSUInteger, STPPaymentMethodType) { + + /** + * Don't use any payment methods except for cards. + */ + STPPaymentMethodTypeNone = 0, + + /** + * The user is allowed to pay with Apple Pay (if it's configured and available on their device). + */ + STPPaymentMethodTypeApplePay = 1 << 0, + /** + * The user can use any available payment method to pay. + */ + STPPaymentMethodTypeAll = STPPaymentMethodTypeApplePay +}; + +/** + * This protocol represents a payment method that a user can select and use to pay. Currently the only classes that conform to it are `STPCard` (which represents that the user wants to pay with a specific card) and `STPApplePayPaymentMethod` (which represents that the user wants to pay with Apple Pay). + */ +@protocol STPPaymentMethod + +/** + * A small (32 x 20 points) logo image representing the payment method. For example, the Visa logo for a Visa card, or the Apple Pay logo. + */ +@property (nonatomic, readonly) UIImage *image; + +/** + * A small (32 x 20 points) logo image representing the payment method that can be used as template for tinted icons. + */ +@property (nonatomic, readonly) UIImage *templateImage; + +/** + * A string describing the payment method, such as "Apple Pay" or "Visa 4242". + */ +@property (nonatomic, readonly) NSString *label; + +@end diff --git a/TelegramUI/STPPhoneNumberValidator.h b/TelegramUI/STPPhoneNumberValidator.h new file mode 100755 index 0000000000..9f775178ba --- /dev/null +++ b/TelegramUI/STPPhoneNumberValidator.h @@ -0,0 +1,31 @@ +// +// STPPhoneNumberValidator.h +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPPhoneNumberValidator : NSObject + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string; ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string; ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string; ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string; ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TelegramUI/STPPhoneNumberValidator.m b/TelegramUI/STPPhoneNumberValidator.m new file mode 100755 index 0000000000..09f83d0e78 --- /dev/null +++ b/TelegramUI/STPPhoneNumberValidator.m @@ -0,0 +1,107 @@ +// +// STPPhoneNumberValidator.m +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPPhoneNumberValidator.h" +#import "STPCardValidator.h" +#import "NSString+Stripe.h" + +@implementation STPPhoneNumberValidator + ++ (NSString *)countryCodeOrCurrentLocaleCountryFromString:(nullable NSString *)nillableCode { + NSString *countryCode = nillableCode; + if (!countryCode) { + countryCode = [[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleCountryCode]; + } + return countryCode; +} + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string { + return [self stringIsValidPartialPhoneNumber:string forCountryCode:nil]; +} + ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string { + return [self stringIsValidPhoneNumber:string forCountryCode:nil]; +} + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + + if ([countryCode isEqualToString:@"US"]) { + return [STPCardValidator sanitizedNumericStringForString:string].length <= 10; + } + else { + return YES; + } +} + ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + + if ([countryCode isEqualToString:@"US"]) { + return [STPCardValidator sanitizedNumericStringForString:string].length == 10; + } + else { + return YES; + } +} + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string { + return [self formattedSanitizedPhoneNumberForString:string + forCountryCode:nil]; +} + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + NSString *sanitized = [STPCardValidator sanitizedNumericStringForString:string]; + return [self formattedPhoneNumberForString:sanitized + forCountryCode:countryCode]; +} + ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string { + return [self formattedRedactedPhoneNumberForString:string + forCountryCode:nil]; +} + ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + NSScanner *scanner = [NSScanner scannerWithString:string]; + NSMutableString *prefix = [NSMutableString stringWithCapacity:string.length]; + [scanner scanUpToString:@"*" intoString:&prefix]; + NSString *number = [string stringByReplacingOccurrencesOfString:prefix withString:@""]; + number = [number stringByReplacingOccurrencesOfString:@"*" withString:@"•"]; + number = [self formattedPhoneNumberForString:number + forCountryCode:countryCode]; + return [NSString stringWithFormat:@"%@ %@", prefix, number]; +} + ++ (NSString *)formattedPhoneNumberForString:(NSString *)string + forCountryCode:(NSString *)countryCode { + + if (![countryCode isEqualToString:@"US"]) { + return string; + } + if (string.length >= 6) { + return [NSString stringWithFormat:@"(%@) %@-%@", + [string stp_safeSubstringToIndex:3], + [[string stp_safeSubstringToIndex:6] stp_safeSubstringFromIndex:3], + [[string stp_safeSubstringToIndex:10] stp_safeSubstringFromIndex:6] + ]; + } else if (string.length >= 3) { + return [NSString stringWithFormat:@"(%@) %@", + [string stp_safeSubstringToIndex:3], + [string stp_safeSubstringFromIndex:3] + ]; + } + return string; +} + +@end diff --git a/TelegramUI/STPPostalCodeValidator.h b/TelegramUI/STPPostalCodeValidator.h new file mode 100755 index 0000000000..fadaa0d36b --- /dev/null +++ b/TelegramUI/STPPostalCodeValidator.h @@ -0,0 +1,26 @@ +// +// STPPostalCodeValidator.h +// Stripe +// +// Created by Ben Guo on 4/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, STPPostalCodeType) { + STPCountryPostalCodeTypeNumericOnly, + STPCountryPostalCodeTypeAlphanumeric, + STPCountryPostalCodeTypeNotRequired, +}; + +@interface STPPostalCodeValidator : NSObject + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + type:(STPPostalCodeType)postalCodeType; ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + countryCode:(nullable NSString *)countryCode; + ++ (STPPostalCodeType)postalCodeTypeForCountryCode:(nullable NSString *)countryCode; + +@end diff --git a/TelegramUI/STPPostalCodeValidator.m b/TelegramUI/STPPostalCodeValidator.m new file mode 100755 index 0000000000..a332ba6a21 --- /dev/null +++ b/TelegramUI/STPPostalCodeValidator.m @@ -0,0 +1,117 @@ +// +// STPPostalCodeValidator.m +// Stripe +// +// Created by Ben Guo on 4/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPostalCodeValidator.h" +#import "STPCardValidator.h" +#import "STPPhoneNumberValidator.h" + +@implementation STPPostalCodeValidator + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + type:(STPPostalCodeType)postalCodeType { + switch (postalCodeType) { + case STPCountryPostalCodeTypeNumericOnly: + return [STPCardValidator sanitizedNumericStringForString:string].length > 0; + case STPCountryPostalCodeTypeAlphanumeric: + return string.length > 0; + case STPCountryPostalCodeTypeNotRequired: + return YES; + } +} + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + countryCode:(nullable NSString *)countryCode { + return [self stringIsValidPostalCode:string + type:[self postalCodeTypeForCountryCode:countryCode]]; +} + ++ (STPPostalCodeType)postalCodeTypeForCountryCode:(NSString *)countryCode { + if ([countryCode isEqualToString:@"US"]) { + return STPCountryPostalCodeTypeNumericOnly; + } + else if ([[self countriesWithNoPostalCodes] containsObject:countryCode]) { + return STPCountryPostalCodeTypeNotRequired; + } + else { + return STPCountryPostalCodeTypeAlphanumeric; + } +} + ++ (NSArray *)countriesWithNoPostalCodes { + return @[ @"AE", + @"AG", + @"AN", + @"AO", + @"AW", + @"BF", + @"BI", + @"BJ", + @"BO", + @"BS", + @"BW", + @"BZ", + @"CD", + @"CF", + @"CG", + @"CI", + @"CK", + @"CM", + @"DJ", + @"DM", + @"ER", + @"FJ", + @"GD", + @"GH", + @"GM", + @"GN", + @"GQ", + @"GY", + @"HK", + @"IE", + @"JM", + @"KE", + @"KI", + @"KM", + @"KN", + @"KP", + @"LC", + @"ML", + @"MO", + @"MR", + @"MS", + @"MU", + @"MW", + @"NR", + @"NU", + @"PA", + @"QA", + @"RW", + @"SA", + @"SB", + @"SC", + @"SL", + @"SO", + @"SR", + @"ST", + @"SY", + @"TF", + @"TK", + @"TL", + @"TO", + @"TT", + @"TV", + @"TZ", + @"UG", + @"VU", + @"YE", + @"ZA", + @"ZW" + ]; +} + +@end diff --git a/TelegramUI/STPSource.h b/TelegramUI/STPSource.h new file mode 100755 index 0000000000..081a0720ba --- /dev/null +++ b/TelegramUI/STPSource.h @@ -0,0 +1,22 @@ +// +// STPSource.h +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import + +/** + * A source represents a source of funds for your user that you can charge - for example, a card on file. Currently, only `STPCard` implements this interface, although future payment methods will use it as well. When implementing your server backend, you should pass the `stripeID` property to the Create Charge method as the `source` parameter. + */ +@protocol STPSource + +/** + * The stripe ID of the source. When implementing your server backend, you should pass this property to the Create Charge method as the `source` parameter. + */ +@property(nonatomic, readonly, copy, nonnull)NSString *stripeID; + +@end diff --git a/TelegramUI/STPToken.h b/TelegramUI/STPToken.h new file mode 100755 index 0000000000..d3956fd53b --- /dev/null +++ b/TelegramUI/STPToken.h @@ -0,0 +1,53 @@ +// +// STPToken.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/5/12. +// +// + +#import +#import "STPAPIResponseDecodable.h" +#import "STPSource.h" + +@class STPCard; +@class STPBankAccount; + +/** + * A token returned from submitting payment details to the Stripe API. You should not have to instantiate one of these directly. + */ +@interface STPToken : NSObject + +/** + * You cannot directly instantiate an `STPToken`. You should only use one that has been returned from an `STPAPIClient` callback. + */ +- (nonnull instancetype) init __attribute__((unavailable("You cannot directly instantiate an STPToken. You should only use one that has been returned from an STPAPIClient callback."))); + +/** + * The value of the token. You can store this value on your server and use it to make charges and customers. @see + * https://stripe.com/docs/mobile/ios#sending-tokens + */ +@property (nonatomic, readonly, nonnull) NSString *tokenId; + +/** + * Whether or not this token was created in livemode. Will be YES if you used your Live Publishable Key, and NO if you used your Test Publishable Key. + */ +@property (nonatomic, readonly) BOOL livemode; + +/** + * The credit card details that were used to create the token. Will only be set if the token was created via a credit card or Apple Pay, otherwise it will be + * nil. + */ +@property (nonatomic, readonly, nullable) STPCard *card; + +/** + * The bank account details that were used to create the token. Will only be set if the token was created with a bank account, otherwise it will be nil. + */ +@property (nonatomic, readonly, nullable) STPBankAccount *bankAccount; + +/** + * When the token was created. + */ +@property (nonatomic, readonly, nullable) NSDate *created; + +@end diff --git a/TelegramUI/STPToken.m b/TelegramUI/STPToken.m new file mode 100755 index 0000000000..1dcd8933b6 --- /dev/null +++ b/TelegramUI/STPToken.m @@ -0,0 +1,101 @@ +// +// STPToken.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/5/12. +// +// + +#import "STPToken.h" +#import "STPCard.h" +#import "STPBankAccount.h" +#import "NSDictionary+Stripe.h" + +@interface STPToken() +@property (nonatomic, nonnull) NSString *tokenId; +@property (nonatomic) BOOL livemode; +@property (nonatomic, nullable) STPCard *card; +@property (nonatomic, nullable) STPBankAccount *bankAccount; +@property (nonatomic, nullable) NSDate *created; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; +@end + +@implementation STPToken + +- (NSString *)description { + return self.tokenId ?: @"Unknown token"; +} + +- (NSString *)debugDescription { + NSString *token = self.tokenId ?: @"Unknown token"; + NSString *livemode = self.livemode ? @"live mode" : @"test mode"; + return [NSString stringWithFormat:@"%@ (%@)", token, livemode]; +} + +- (BOOL)isEqual:(id)object { + return [self isEqualToToken:object]; +} + +- (NSUInteger)hash { + return [self.tokenId hash]; +} + +- (BOOL)isEqualToToken:(STPToken *)object { + if (self == object) { + return YES; + } + + if (!object || ![object isKindOfClass:self.class]) { + return NO; + } + + if ((self.card || object.card) && (![self.card isEqual:object.card])) { + return NO; + } + + if ((self.bankAccount || object.bankAccount) && (![self.bankAccount isEqual:object.bankAccount])) { + return NO; + } + + return self.livemode == object.livemode && [self.tokenId isEqualToString:object.tokenId] && [self.created isEqualToDate:object.created] && + [self.card isEqual:object.card] && [self.tokenId isEqualToString:object.tokenId] && [self.created isEqualToDate:object.created]; +} + +#pragma mark STPSource + +- (NSString *)stripeID { + return self.tokenId; +} + +#pragma mark STPAPIResponseDecodable + ++ (NSArray *)requiredFields { + return @[@"id", @"livemode", @"created"]; +} + ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPToken *token = [self new]; + token.tokenId = dict[@"id"]; + token.livemode = [dict[@"livemode"] boolValue]; + token.created = [NSDate dateWithTimeIntervalSince1970:[dict[@"created"] doubleValue]]; + + NSDictionary *cardDictionary = dict[@"card"]; + if (cardDictionary) { + token.card = [STPCard decodedObjectFromAPIResponse:cardDictionary]; + } + + NSDictionary *bankAccountDictionary = dict[@"bank_account"]; + if (bankAccountDictionary) { + token.bankAccount = [STPBankAccount decodedObjectFromAPIResponse:bankAccountDictionary]; + } + + token.allResponseFields = dict; + return token; +} + +@end diff --git a/TelegramUI/STPWeakStrongMacros.h b/TelegramUI/STPWeakStrongMacros.h new file mode 100755 index 0000000000..e090126cda --- /dev/null +++ b/TelegramUI/STPWeakStrongMacros.h @@ -0,0 +1,19 @@ +// +// STPWeakStrongMacros.h +// Stripe +// +// Created by Brian Dorfman on 7/28/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +/* + * Based on @weakify() and @strongify() from + * https://github.com/jspahrsummers/libextc + */ + +#define WEAK(var) __weak typeof(var) weak_##var = var; +#define STRONG(var) \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +__strong typeof(var) var = weak_##var; \ +_Pragma("clang diagnostic pop") \ diff --git a/TelegramUI/SelectivePrivacySettingsController.swift b/TelegramUI/SelectivePrivacySettingsController.swift index d297c5fbe3..fe31b38348 100644 --- a/TelegramUI/SelectivePrivacySettingsController.swift +++ b/TelegramUI/SelectivePrivacySettingsController.swift @@ -431,7 +431,7 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy (controller?.navigationController as? NavigationController)?.pushViewController(c) } presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window) + controller?.present(c, in: .window(.root)) } dismissImpl = { [weak controller] in let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) diff --git a/TelegramUI/SelectivePrivacySettingsPeersController.swift b/TelegramUI/SelectivePrivacySettingsPeersController.swift index 11dc31771e..c6efa6f07b 100644 --- a/TelegramUI/SelectivePrivacySettingsPeersController.swift +++ b/TelegramUI/SelectivePrivacySettingsPeersController.swift @@ -311,7 +311,7 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } return controller diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 1e99e18e42..c3bea4cbd3 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -587,7 +587,7 @@ public func settingsController(account: Account, accountManager: AccountManager) (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value, arguments in - controller?.present(value, in: .window, with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { diff --git a/TelegramUI/SharedMediaPlayer.swift b/TelegramUI/SharedMediaPlayer.swift new file mode 100644 index 0000000000..d39e1d2fde --- /dev/null +++ b/TelegramUI/SharedMediaPlayer.swift @@ -0,0 +1,221 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +enum SharedMediaPlayerControlAction { + case next + case previous + case play + case pause + case togglePlayPause +} + +enum SharedMediaPlaylistControlAction { + case next + case previous +} + +enum SharedMediaPlaybackDataType { + case music + case voice + case instantVideo +} + +enum SharedMediaPlaybackDataSource: Equatable { + case telegramFile(TelegramMediaFile) + + static func ==(lhs: SharedMediaPlaybackDataSource, rhs: SharedMediaPlaybackDataSource) -> Bool { + switch lhs { + case let .telegramFile(lhsFile): + if case let .telegramFile(rhsFile) = rhs { + return lhsFile.isEqual(rhsFile) + } else { + return false + } + } + } +} + +struct SharedMediaPlaybackData: Equatable { + let type: SharedMediaPlaybackDataType + let source: SharedMediaPlaybackDataSource + + static func ==(lhs: SharedMediaPlaybackData, rhs: SharedMediaPlaybackData) -> Bool { + return lhs.type == rhs.type && lhs.source == rhs.source + } +} + +enum SharedMediaPlaybackDisplayData: Equatable { + case music(title: String?, performer: String?) + case voice(author: Peer?, peer: Peer?) + case instantVideo(author: Peer?, peer: Peer?) + + static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> 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 let .voice(lhsAuthor, lhsPeer): + if case let .voice(rhsAuthor, rhsPeer) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer) { + return true + } else { + return false + } + case let .instantVideo(lhsAuthor, lhsPeer): + if case let .instantVideo(rhsAuthor, rhsPeer) = rhs, arePeersEqual(lhsAuthor, rhsAuthor), arePeersEqual(lhsPeer, rhsPeer) { + return true + } else { + return false + } + } + } +} + +protocol SharedMediaPlaylistItem { + var stableId: AnyHashable { get } + var playbackData: SharedMediaPlaybackData? { get } + var displayData: SharedMediaPlaybackDisplayData? { get } +} + +final class SharedMediaPlaylistState { + let loading: Bool + let item: SharedMediaPlaylistItem? + + init(loading: Bool, item: SharedMediaPlaylistItem?) { + self.loading = loading + self.item = item + } +} + +protocol SharedMediaPlaylist { + var state: Signal { get } + + func control(_ action: SharedMediaPlaylistControlAction) +} + +private enum SharedMediaPlaybackItem { + case audio(MediaPlayer) + case instantVideo(InstantVideoNode) + + func play() { + switch self { + case let .audio(player): + player.play() + case let .instantVideo(node): + node.play() + } + } + + func pause() { + switch self { + case let .audio(player): + player.pause() + case let .instantVideo(node): + node.pause() + } + } + + func togglePlayPause() { + switch self { + case let .audio(player): + player.togglePlayPause() + case let .instantVideo(node): + node.togglePlayPause() + } + } + + func seek(_ timestamp: Double) { + switch self { + case let .audio(player): + player.seek(timestamp: timestamp) + case let .instantVideo(node): + node.seek(timestamp) + } + } + + func setSoundEnabled(_ value: Bool) { + switch self { + case .audio: + break + case let .instantVideo(node): + node.setSoundEnabled(value) + } + } +} + +final class SharedMediaPlayer { + private let account: Account + private let manager: MediaManager + private let playlist: SharedMediaPlaylist + + private var stateDisposable: Disposable? + + private var stateValue: SharedMediaPlaylistState? + private var playbackItem: SharedMediaPlaybackItem? + + init(account: Account, manager: MediaManager, playlist: SharedMediaPlaylist) { + self.account = account + self.manager = manager + self.playlist = playlist + + self.stateDisposable = (playlist.state |> deliverOnMainQueue).start(next: { [weak self] state in + if let strongSelf = self { + if state.item?.playbackData != strongSelf.stateValue?.item?.playbackData { + strongSelf.playbackItem?.pause() + if let playbackItem = strongSelf.playbackItem { + switch playbackItem { + case .audio: + break + case let .instantVideo(node): + strongSelf.manager.overlayMediaManager.controller?.removeNode(node) + } + } + strongSelf.playbackItem = nil + if let item = state.item, let playbackData = item.playbackData { + switch playbackData.type { + case .voice, .music: + switch playbackData.source { + case let .telegramFile(file): + strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.manager.audioSession, postbox: strongSelf.account.postbox, resource: file.resource, streamable: true, video: false, preferSoftwareDecoding: false, enableSound: true)) + } + case .instantVideo: + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + switch playbackData.source { + case let .telegramFile(file): + strongSelf.playbackItem = .instantVideo(InstantVideoNode(theme: presentationData.theme, manager: strongSelf.manager, account: strongSelf.account, source: .messageMedia(stableId: item.stableId, file: file), priority: 0, withSound: true)) + } + } + } + if let playbackItem = strongSelf.playbackItem { + switch playbackItem { + case .audio: + break + case let .instantVideo(node): + strongSelf.manager.overlayMediaManager.controller?.addNode(node) + } + } + } + strongSelf.stateValue = state + } + }) + } + + deinit { + self.stateDisposable?.dispose() + } + + func control(_ action: SharedMediaPlayerControlAction) { + switch action { + case .next: + self.playlist.control(.next) + case .previous: + self.playlist.control(.previous) + case .play, .pause, .togglePlayPause: + break + } + } +} diff --git a/TelegramUI/SharedVideoContextManager.swift b/TelegramUI/SharedVideoContextManager.swift new file mode 100644 index 0000000000..d83bc04ca7 --- /dev/null +++ b/TelegramUI/SharedVideoContextManager.swift @@ -0,0 +1,120 @@ +import Foundation +import AsyncDisplayKit +import SwiftSignalKit + +class SharedVideoContext { + func dispose() { + } +} + +private final class SharedVideoContextSubscriber { + let id: Int32 + let priority: Int32 + let update: (SharedVideoContext?) -> Void + var active: Bool = false + + init(id: Int32, priority: Int32, update: @escaping (SharedVideoContext?) -> Void) { + self.id = id + self.priority = priority + self.update = update + } +} + +private final class SharedVideoContextHolder { + private var nextId: Int32 = 0 + private var subscribers: [SharedVideoContextSubscriber] = [] + let context: SharedVideoContext + + init(context: SharedVideoContext) { + self.context = context + } + + var isEmpty: Bool { + return self.subscribers.isEmpty + } + + func addSubscriber(priority: Int32, update: @escaping (SharedVideoContext?) -> Void) -> Int32 { + let id = self.nextId + self.nextId += 1 + + self.subscribers.append(SharedVideoContextSubscriber(id: id, priority: priority, update: update)) + self.subscribers.sort(by: { lhs, rhs in + if lhs.priority != rhs.priority { + return lhs.priority < rhs.priority + } + return lhs.id < rhs.id + }) + + return id + } + + func removeSubscriberAndUpdate(id: Int32) { + for i in 0 ..< self.subscribers.count { + if self.subscribers[i].id == id { + let subscriber = self.subscribers[i] + self.subscribers.remove(at: i) + if subscriber.active { + subscriber.update(nil) + self.update() + } + break + } + } + } + + func update() { + for i in (0 ..< self.subscribers.count) { + if i == self.subscribers.count - 1 { + if !self.subscribers[i].active { + self.subscribers[i].active = true + self.subscribers[i].update(self.context) + } + } else { + if self.subscribers[i].active { + self.subscribers[i].active = false + self.subscribers[i].update(nil) + } + } + } + } +} + +final class SharedVideoContextManager { + private var holders: [AnyHashable: SharedVideoContextHolder] = [:] + + func attachSharedVideoContext(id: AnyHashable, priority: Int32, create: () -> SharedVideoContext, update: @escaping (SharedVideoContext?) -> Void) -> Int32 { + assert(Queue.mainQueue().isCurrent()) + + let holder: SharedVideoContextHolder + if let current = self.holders[id] { + holder = current + } else { + holder = SharedVideoContextHolder(context: create()) + self.holders[id] = holder + } + + let id = holder.addSubscriber(priority: priority, update: update) + holder.update() + return id + } + + func detachSharedVideoContext(id: AnyHashable, index: Int32) { + assert(Queue.mainQueue().isCurrent()) + + if let holder = self.holders[id] { + holder.removeSubscriberAndUpdate(id: index) + if holder.isEmpty { + holder.context.dispose() + self.holders.removeValue(forKey: id) + } + } + } + + func withSharedVideoContext(id: AnyHashable, _ f: (SharedVideoContext?) -> Void) { + if let holder = self.holders[id] { + f(holder.context) + } else { + f(nil) + } + } +} diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index 84ac2ed605..cce0aed21a 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -52,7 +52,7 @@ final class StickerPackPreviewController: ViewController { self?.dismiss() } self.controllerNode.presentPreview = { [weak self] controller, arguments in - self?.present(controller, in: .window, with: arguments) + self?.present(controller, in: .window(.root), with: arguments) } self.controllerNode.sendSticker = { [weak self] file in self?.sendSticker?(file) diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 24ebb12505..54ec92a9af 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -374,7 +374,7 @@ func storageUsageController(account: Account) -> ViewController { let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } return controller diff --git a/TelegramUI/StripeError.h b/TelegramUI/StripeError.h new file mode 100755 index 0000000000..9cf9ef99e5 --- /dev/null +++ b/TelegramUI/StripeError.h @@ -0,0 +1,74 @@ +// +// StripeError.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/4/12. +// +// + +#import + +/** + * All Stripe iOS errors will be under this domain. + */ +FOUNDATION_EXPORT NSString * __nonnull const StripeDomain; + +typedef NS_ENUM(NSInteger, STPErrorCode) { + STPConnectionError = 40, // Trouble connecting to Stripe. + STPInvalidRequestError = 50, // Your request had invalid parameters. + STPAPIError = 60, // General-purpose API error (should be rare). + STPCardError = 70, // Something was wrong with the given card (most common). + STPCancellationError = 80, // The operation was cancelled. + STPCheckoutUnknownError = 5000, // Checkout failed + STPCheckoutTooManyAttemptsError = 5001, // Too many incorrect code attempts +}; + +#pragma mark userInfo keys + +// A developer-friendly error message that explains what went wrong. You probably +// shouldn't show this to your users, but might want to use it yourself. +FOUNDATION_EXPORT NSString * __nonnull const STPErrorMessageKey; + +// What went wrong with your STPCard (e.g., STPInvalidCVC. See below for full list). +FOUNDATION_EXPORT NSString * __nonnull const STPCardErrorCodeKey; + +// Which parameter on the STPCard had an error (e.g., "cvc"). Useful for marking up the +// right UI element. +FOUNDATION_EXPORT NSString * __nonnull const STPErrorParameterKey; + +#pragma mark STPCardErrorCodeKeys + +// (Usually determined locally:) +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidNumber; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidExpMonth; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidExpYear; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidCVC; + +// (Usually sent from the server:) +FOUNDATION_EXPORT NSString * __nonnull const STPIncorrectNumber; +FOUNDATION_EXPORT NSString * __nonnull const STPExpiredCard; +FOUNDATION_EXPORT NSString * __nonnull const STPCardDeclined; +FOUNDATION_EXPORT NSString * __nonnull const STPProcessingError; +FOUNDATION_EXPORT NSString * __nonnull const STPIncorrectCVC; + + +@interface NSError(Stripe) + ++ (nullable NSError *)stp_errorFromStripeResponse:(nullable NSDictionary *)jsonDictionary; ++ (nonnull NSError *)stp_genericFailedToParseResponseError; +- (BOOL)stp_isUnknownCheckoutError; +- (BOOL)stp_isURLSessionCancellationError; + +#pragma mark Strings + ++ (nonnull NSString *)stp_cardErrorInvalidNumberUserMessage; ++ (nonnull NSString *)stp_cardInvalidCVCUserMessage; ++ (nonnull NSString *)stp_cardErrorInvalidExpMonthUserMessage; ++ (nonnull NSString *)stp_cardErrorInvalidExpYearUserMessage; ++ (nonnull NSString *)stp_cardErrorExpiredCardUserMessage; ++ (nonnull NSString *)stp_cardErrorDeclinedUserMessage; ++ (nonnull NSString *)stp_cardErrorProcessingErrorUserMessage; ++ (nonnull NSString *)stp_unexpectedErrorMessage; + + +@end diff --git a/TelegramUI/StripeError.m b/TelegramUI/StripeError.m new file mode 100755 index 0000000000..d503524da2 --- /dev/null +++ b/TelegramUI/StripeError.m @@ -0,0 +1,137 @@ +// +// StripeError.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/4/12. +// +// + +#import "StripeError.h" +#import "STPFormEncoder.h" + +NSString *const StripeDomain = @"com.stripe.lib"; +NSString *const STPCardErrorCodeKey = @"com.stripe.lib:CardErrorCodeKey"; +NSString *const STPErrorMessageKey = @"com.stripe.lib:ErrorMessageKey"; +NSString *const STPErrorParameterKey = @"com.stripe.lib:ErrorParameterKey"; +NSString *const STPInvalidNumber = @"com.stripe.lib:InvalidNumber"; +NSString *const STPInvalidExpMonth = @"com.stripe.lib:InvalidExpiryMonth"; +NSString *const STPInvalidExpYear = @"com.stripe.lib:InvalidExpiryYear"; +NSString *const STPInvalidCVC = @"com.stripe.lib:InvalidCVC"; +NSString *const STPIncorrectNumber = @"com.stripe.lib:IncorrectNumber"; +NSString *const STPExpiredCard = @"com.stripe.lib:ExpiredCard"; +NSString *const STPCardDeclined = @"com.stripe.lib:CardDeclined"; +NSString *const STPProcessingError = @"com.stripe.lib:ProcessingError"; +NSString *const STPIncorrectCVC = @"com.stripe.lib:IncorrectCVC"; + +@implementation NSError(Stripe) + ++ (NSError *)stp_errorFromStripeResponse:(NSDictionary *)jsonDictionary { + NSDictionary *errorDictionary = jsonDictionary[@"error"]; + if (!errorDictionary) { + return nil; + } + NSString *type = errorDictionary[@"type"]; + NSString *devMessage = errorDictionary[@"message"]; + NSString *parameter = errorDictionary[@"param"]; + NSInteger code = 0; + + // There should always be a message and type for the error + if (devMessage == nil || type == nil) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: [self stp_unexpectedErrorMessage], + STPErrorMessageKey: @"Could not interpret the error response that was returned from Stripe." + }; + return [[self alloc] initWithDomain:StripeDomain code:STPAPIError userInfo:userInfo]; + } + + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[STPErrorMessageKey] = devMessage; + + if (parameter) { + userInfo[STPErrorParameterKey] = [STPFormEncoder stringByReplacingSnakeCaseWithCamelCase:parameter]; + } + + if ([type isEqualToString:@"api_error"]) { + code = STPAPIError; + userInfo[NSLocalizedDescriptionKey] = [self stp_unexpectedErrorMessage]; + } else if ([type isEqualToString:@"invalid_request_error"]) { + code = STPInvalidRequestError; + userInfo[NSLocalizedDescriptionKey] = devMessage; + } else if ([type isEqualToString:@"card_error"]) { + code = STPCardError; + NSDictionary *errorCodes = @{ + @"incorrect_number": @{@"code": STPIncorrectNumber, @"message": [self stp_cardErrorInvalidNumberUserMessage]}, + @"invalid_number": @{@"code": STPInvalidNumber, @"message": [self stp_cardErrorInvalidNumberUserMessage]}, + @"invalid_expiry_month": @{@"code": STPInvalidExpMonth, @"message": [self stp_cardErrorInvalidExpMonthUserMessage]}, + @"invalid_expiry_year": @{@"code": STPInvalidExpYear, @"message": [self stp_cardErrorInvalidExpYearUserMessage]}, + @"invalid_cvc": @{@"code": STPInvalidCVC, @"message": [self stp_cardInvalidCVCUserMessage]}, + @"expired_card": @{@"code": STPExpiredCard, @"message": [self stp_cardErrorExpiredCardUserMessage]}, + @"incorrect_cvc": @{@"code": STPIncorrectCVC, @"message": [self stp_cardInvalidCVCUserMessage]}, + @"card_declined": @{@"code": STPCardDeclined, @"message": [self stp_cardErrorDeclinedUserMessage]}, + @"processing_error": @{@"code": STPProcessingError, @"message": [self stp_cardErrorProcessingErrorUserMessage]}, + }; + NSDictionary *codeMapEntry = errorCodes[errorDictionary[@"code"]]; + + if (codeMapEntry) { + userInfo[STPCardErrorCodeKey] = codeMapEntry[@"code"]; + userInfo[NSLocalizedDescriptionKey] = codeMapEntry[@"message"]; + } else { + userInfo[STPCardErrorCodeKey] = errorDictionary[@"code"]; + userInfo[NSLocalizedDescriptionKey] = devMessage; + } + } + + return [[self alloc] initWithDomain:StripeDomain code:code userInfo:userInfo]; +} + ++ (nonnull NSError *)stp_genericFailedToParseResponseError { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: [self stp_unexpectedErrorMessage], + STPErrorMessageKey: @"The response from Stripe failed to get parsed into valid JSON." + }; + return [[self alloc] initWithDomain:StripeDomain code:STPAPIError userInfo:userInfo]; +} + +- (BOOL)stp_isUnknownCheckoutError { + return self.code == STPCheckoutUnknownError; +} + +- (BOOL)stp_isURLSessionCancellationError { + return [self.domain isEqualToString:NSURLErrorDomain] && self.code == NSURLErrorCancelled; +} + +#pragma mark Strings + ++ (nonnull NSString *)stp_cardErrorInvalidNumberUserMessage { + return @"Your_cards_number_is_invalid"; +} + ++ (nonnull NSString *)stp_cardInvalidCVCUserMessage { + return @"Your_cards_security_code_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorInvalidExpMonthUserMessage { + return @"Your_cards_expiration_month_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorInvalidExpYearUserMessage { + return @"Your_cards_expiration_year_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorExpiredCardUserMessage { + return @"Your_card_has_expired"; +} + ++ (nonnull NSString *)stp_cardErrorDeclinedUserMessage { + return @"Your_card_was_declined"; +} + ++ (nonnull NSString *)stp_unexpectedErrorMessage { + return @"Error.Generic"; +} + ++ (nonnull NSString *)stp_cardErrorProcessingErrorUserMessage { + return @"Error.Generic"; +} + +@end diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index cc18bd54e6..81915d1ae8 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -27,6 +27,8 @@ public final class TelegramApplicationContext { public let mediaManager = MediaManager() + public let contactsManager = DeviceContactsManager() + public let currentPresentationData: Atomic private let _presentationData = Promise() public var presentationData: Signal { diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index c70b68e29f..5f7e21c55d 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -13,4 +13,10 @@ module TelegramUIPrivateModule { header "../RingBuffer.h" header "../TelegramUIIncludes.h" header "../../third-party/RMIntro/platform/ios/RMIntroViewController.h" + header "../STPPaymentCardTextField.h" + header "../STPAPIClient.h" + header "../STPAPIClient+ApplePay.h" + header "../STPPaymentConfiguration.h" + header "../STPCard.h" + header "../STPToken.h" } diff --git a/TelegramUI/TelegramVideoNode.swift b/TelegramUI/TelegramVideoNode.swift new file mode 100644 index 0000000000..5d88e9a10f --- /dev/null +++ b/TelegramUI/TelegramVideoNode.swift @@ -0,0 +1,527 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +import TelegramLegacyComponents + +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) + 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() + } 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(account: account, signal: chatMessageVideo(account: account, video: source.file)) + + self.statusDisposable = (chatMessageFileStatus(account: account, file: source.file) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + + } + }) + } + + 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/TextNode.swift b/TelegramUI/TextNode.swift index 39a146fbef..8a864d0fba 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -276,16 +276,28 @@ final class TextNode: ASDisplayNode { let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) + var isLastLine = false if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { + isLastLine = true + } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { + isLastLine = true + } + + if isLastLine { if first { first = false } else { layoutSize.height += fontLineSpacing } + let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + + if lineRange.length == 0 { + break + } + let coreTextLine: CTLine - let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { diff --git a/TelegramUI/ThemeGalleryController.swift b/TelegramUI/ThemeGalleryController.swift index 39ad1d12ce..578d41dec9 100644 --- a/TelegramUI/ThemeGalleryController.swift +++ b/TelegramUI/ThemeGalleryController.swift @@ -178,7 +178,7 @@ class ThemeGalleryController: ViewController { override func loadDisplayNode() { let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in if let strongSelf = self { - strongSelf.present(controller, in: .window, with: arguments) + strongSelf.present(controller, in: .window(.root), with: arguments) } }, dismissController: { [weak self] in self?.dismiss(forceAway: true) diff --git a/TelegramUI/ThemeGridController.swift b/TelegramUI/ThemeGridController.swift index 0810a0cf0b..12f7aa24e5 100644 --- a/TelegramUI/ThemeGridController.swift +++ b/TelegramUI/ThemeGridController.swift @@ -65,7 +65,7 @@ final class ThemeGridController: TelegramController { override func loadDisplayNode() { self.displayNode = ThemeGridControllerNode(account: self.account, presentationData: self.presentationData, present: { [weak self] controller, arguments in - self?.present(controller, in: .window, with: arguments) + self?.present(controller, in: .window(.root), with: arguments) }) self._ready.set(self.controllerNode.ready.get()) diff --git a/TelegramUI/TwoStepVerificationPasswordEntryController.swift b/TelegramUI/TwoStepVerificationPasswordEntryController.swift index ae3472c4c0..ba33e7b65d 100644 --- a/TelegramUI/TwoStepVerificationPasswordEntryController.swift +++ b/TelegramUI/TwoStepVerificationPasswordEntryController.swift @@ -424,7 +424,7 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } dismissImpl = { [weak controller] in diff --git a/TelegramUI/TwoStepVerificationResetController.swift b/TelegramUI/TwoStepVerificationResetController.swift index 71d53b1d3d..e447527a73 100644 --- a/TelegramUI/TwoStepVerificationResetController.swift +++ b/TelegramUI/TwoStepVerificationResetController.swift @@ -223,7 +223,7 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } dismissImpl = { [weak controller] in diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index c552c81513..517558f002 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -530,7 +530,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep } presentControllerImpl = { [weak controller] c, p in if let controller = controller { - controller.present(c, in: .window, with: p) + controller.present(c, in: .window(.root), with: p) } } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 8002910ac4..0ce80a8099 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -409,16 +409,17 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat entries.append(UserInfoEntry.groupsInCommon(presentationData.theme, presentationData.strings.UserInfo_GroupsInCommon, groupsInCommon)) } - if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint { - entries.append(UserInfoEntry.secretEncryptionKey(presentationData.theme, presentationData.strings.Profile_EncryptionKey, keyFingerprint)) - } - if isEditing { entries.append(UserInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, "Default")) + if view.peerIsContact { entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact), .removeContact)) } } else { + if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint { + entries.append(UserInfoEntry.secretEncryptionKey(presentationData.theme, presentationData.strings.Profile_EncryptionKey, keyFingerprint)) + } + if let cachedData = view.cachedData as? CachedUserData { if cachedData.isBlocked { entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock), .unblock)) @@ -636,7 +637,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value, presentationArguments in - controller?.present(value, in: .window, with: presentationArguments) + controller?.present(value, in: .window(.root), with: presentationArguments) } openChatImpl = { [weak controller] in if let navigationController = (controller?.navigationController as? NavigationController) { @@ -661,7 +662,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { UIPasteboard.general.string = text })]) - strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let resultItemNode = resultItemNode { return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) } else { diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift index 9d3b85f5d7..053bd73cc5 100644 --- a/TelegramUI/UsernameSetupController.swift +++ b/TelegramUI/UsernameSetupController.swift @@ -291,6 +291,7 @@ public func usernameSetupController(account: Account) -> ViewController { } let controller = ItemListController(account: account, state: signal) + controller.enableInteractiveDismiss = true dismissImpl = { [weak controller] in controller?.dismiss() } diff --git a/TelegramUI/VideoOverlayMediaItem.swift b/TelegramUI/VideoOverlayMediaItem.swift deleted file mode 100644 index 504084e018..0000000000 --- a/TelegramUI/VideoOverlayMediaItem.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit - -final class VideoOverlayMediaItem: OverlayMediaItem { - fileprivate weak var player: MediaPlayer? - - init(player: MediaPlayer) { - self.player = player - } - - func node() -> OverlayMediaItemNode { - return VideoOverlayMediaItemNode(item: self) - } -} - -final class VideoOverlayMediaItemNode: OverlayMediaItemNode { - private let item: VideoOverlayMediaItem - - init(item: VideoOverlayMediaItem) { - self.item = item - - super.init() - - self.backgroundColor = .green - } -}