diff --git a/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json b/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json new file mode 100644 index 0000000000..cde960dfed --- /dev/null +++ b/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Information Icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf b/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf new file mode 100644 index 0000000000..601a593a3f Binary files /dev/null and b/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf differ diff --git a/Images.xcassets/Chat List/RevealActionReadIcon.imageset/Contents.json b/Images.xcassets/Chat List/RevealActionReadIcon.imageset/Contents.json new file mode 100644 index 0000000000..67dd741c1b --- /dev/null +++ b/Images.xcassets/Chat List/RevealActionReadIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_chats_read.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/RevealActionReadIcon.imageset/ic_chats_read.pdf b/Images.xcassets/Chat List/RevealActionReadIcon.imageset/ic_chats_read.pdf new file mode 100644 index 0000000000..78e948ac13 Binary files /dev/null and b/Images.xcassets/Chat List/RevealActionReadIcon.imageset/ic_chats_read.pdf differ diff --git a/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/Contents.json b/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/Contents.json new file mode 100644 index 0000000000..f61bf6cfe6 --- /dev/null +++ b/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_chats_unread.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/ic_chats_unread.pdf b/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/ic_chats_unread.pdf new file mode 100644 index 0000000000..0b0d32f1c2 Binary files /dev/null and b/Images.xcassets/Chat List/RevealActionUnreadIcon.imageset/ic_chats_unread.pdf differ diff --git a/Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json b/Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json index c84a6db8ab..7e7946ae1b 100644 --- a/Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json +++ b/Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ModernConversationEmptyListLogo@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "telegram_plane_flat.pdf" } ], "info" : { diff --git a/Images.xcassets/Chat/EmptyChatIcon.imageset/ModernConversationEmptyListLogo@2x.png b/Images.xcassets/Chat/EmptyChatIcon.imageset/ModernConversationEmptyListLogo@2x.png deleted file mode 100644 index f12d8d290d..0000000000 Binary files a/Images.xcassets/Chat/EmptyChatIcon.imageset/ModernConversationEmptyListLogo@2x.png and /dev/null differ diff --git a/Images.xcassets/Chat/EmptyChatIcon.imageset/telegram_plane_flat.pdf b/Images.xcassets/Chat/EmptyChatIcon.imageset/telegram_plane_flat.pdf new file mode 100644 index 0000000000..7c0cc20798 Binary files /dev/null and b/Images.xcassets/Chat/EmptyChatIcon.imageset/telegram_plane_flat.pdf differ diff --git a/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json new file mode 100644 index 0000000000..32243bea94 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickerKeyboardGifIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickerKeyboardGifIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png new file mode 100644 index 0000000000..382c35e033 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png new file mode 100644 index 0000000000..d760fa4947 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/GifsTabIcon.imageset/StickerKeyboardGifIcon@3x.png differ diff --git a/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json new file mode 100644 index 0000000000..715abcaaf5 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickerKeyboardRecentTab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickerKeyboardRecentTab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png new file mode 100644 index 0000000000..95cc50e35f Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png new file mode 100644 index 0000000000..045a394615 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/RecentTabIcon.imageset/StickerKeyboardRecentTab@3x.png differ diff --git a/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/Contents.json b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/Contents.json new file mode 100644 index 0000000000..d9172b6bd8 --- /dev/null +++ b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reverse@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reverse@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@2x.png b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@2x.png new file mode 100644 index 0000000000..260b39677a Binary files /dev/null and b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@2x.png differ diff --git a/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@3x.png b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@3x.png new file mode 100644 index 0000000000..f5aa5bc941 Binary files /dev/null and b/Images.xcassets/Secure ID/DocumentInputBackSide.imageset/reverse@3x.png differ diff --git a/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/Contents.json b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/Contents.json new file mode 100644 index 0000000000..320424c9d6 --- /dev/null +++ b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "selfie@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "selfie@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@2x.png b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@2x.png new file mode 100644 index 0000000000..b679ff8dd6 Binary files /dev/null and b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@2x.png differ diff --git a/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@3x.png b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@3x.png new file mode 100644 index 0000000000..25507a4706 Binary files /dev/null and b/Images.xcassets/Secure ID/DocumentInputSelfie.imageset/selfie@3x.png differ diff --git a/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/Contents.json b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/Contents.json new file mode 100644 index 0000000000..6a5f1d4e7e --- /dev/null +++ b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "driver@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "driver@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@2x.png b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@2x.png new file mode 100644 index 0000000000..e9d34a44fe Binary files /dev/null and b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@2x.png differ diff --git a/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@3x.png b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@3x.png new file mode 100644 index 0000000000..c4634dfe42 Binary files /dev/null and b/Images.xcassets/Secure ID/DriversLicenseInputFrontSide.imageset/driver@3x.png differ diff --git a/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/Contents.json b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/Contents.json new file mode 100644 index 0000000000..e37073d68f --- /dev/null +++ b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "idcard@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "idcard@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@2x.png b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@2x.png new file mode 100644 index 0000000000..2a7c5c90aa Binary files /dev/null and b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@2x.png differ diff --git a/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@3x.png b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@3x.png new file mode 100644 index 0000000000..24b152d0c6 Binary files /dev/null and b/Images.xcassets/Secure ID/IdCardInputFrontSide.imageset/idcard@3x.png differ diff --git a/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/Contents.json b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/Contents.json new file mode 100644 index 0000000000..5c3db8358a --- /dev/null +++ b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "passport@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "passport@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@2x.png b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@2x.png new file mode 100644 index 0000000000..889004beb1 Binary files /dev/null and b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@2x.png differ diff --git a/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@3x.png b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@3x.png new file mode 100644 index 0000000000..9a8bc05055 Binary files /dev/null and b/Images.xcassets/Secure ID/PassportInputFrontSide.imageset/passport@3x.png differ diff --git a/Images.xcassets/Secure ID/ViewPassportIcon.imageset/Contents.json b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/Contents.json new file mode 100644 index 0000000000..adc28fe30b --- /dev/null +++ b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PassportSettingsLogo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PassportSettingsLogo@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@2x.png b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@2x.png new file mode 100644 index 0000000000..42c6e76ef7 Binary files /dev/null and b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@2x.png differ diff --git a/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@3x.png b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@3x.png new file mode 100644 index 0000000000..431664e562 Binary files /dev/null and b/Images.xcassets/Secure ID/ViewPassportIcon.imageset/PassportSettingsLogo@3x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Passport.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Passport.imageset/Contents.json new file mode 100644 index 0000000000..b1c5d8d5d4 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Passport.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SettingsPassportIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SettingsPassportIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@2x.png b/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@2x.png new file mode 100644 index 0000000000..8666b97562 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@3x.png b/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@3x.png new file mode 100644 index 0000000000..3a657c20ec Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Passport.imageset/SettingsPassportIcon@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 5eeda93104..d40a5b68bb 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */; }; D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */; }; D0104F2C1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */; }; + D0119CD020CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0119CCF20CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift */; }; + D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D013630B208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift */; }; D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0147BA6206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift */; }; D0147BA9206EA35000E40378 /* SecureIdDocumentGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0147BA8206EA35000E40378 /* SecureIdDocumentGalleryController.swift */; }; D0147BAB206EA6C100E40378 /* SecureIdDocumentImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0147BAA206EA6C100E40378 /* SecureIdDocumentImageGalleryItem.swift */; }; @@ -59,6 +61,7 @@ D01C06C01FBF118A001561AB /* MessageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C06BF1FBF118A001561AB /* MessageUtils.swift */; }; D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */; }; D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */; }; + D01DBA9B209CC6AD00C64E64 /* ChatLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01DBA9A209CC6AD00C64E64 /* ChatLinkPreview.swift */; }; D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */ = {isa = PBXBuildFile; fileRef = D0208AD31FA33D14001F0D5F /* RaiseToListenActivator.h */; }; D0208AD61FA33D14001F0D5F /* RaiseToListenActivator.m in Sources */ = {isa = PBXBuildFile; fileRef = D0208AD41FA33D14001F0D5F /* RaiseToListenActivator.m */; }; D0208AD91FA34017001F0D5F /* DeviceProximityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D0208AD71FA34017001F0D5F /* DeviceProximityManager.h */; }; @@ -107,6 +110,8 @@ D0428200200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281FF200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift */; }; D0430B001FF4570500A35ADD /* WebController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430AFF1FF4570500A35ADD /* WebController.swift */; }; D0430B021FF4584100A35ADD /* WebControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430B011FF4584100A35ADD /* WebControllerNode.swift */; }; + D044A0F320BDA05800326FAC /* ThrottledValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D044A0F220BDA05800326FAC /* ThrottledValue.swift */; }; + D044A0FB20BDC40C00326FAC /* CachedChannelAdmins.swift in Sources */ = {isa = PBXBuildFile; fileRef = D044A0FA20BDC40C00326FAC /* CachedChannelAdmins.swift */; }; D046142E2004DB3700EC0EF2 /* LiveLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D046142D2004DB3700EC0EF2 /* LiveLocationManager.swift */; }; D04614372005094E00EC0EF2 /* DeviceLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04614362005094E00EC0EF2 /* DeviceLocationManager.swift */; }; D0461439200514F000EC0EF2 /* LiveLocationSummaryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0461438200514F000EC0EF2 /* LiveLocationSummaryManager.swift */; }; @@ -161,9 +166,12 @@ D06BEC771F62F68B0035A545 /* OverlayUniversalVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */; }; D06BEC8A1F6597A80035A545 /* OverlayVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */; }; D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */; }; + D06CF82720D0080200AC4CFF /* SecureIdAuthListContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06CF82620D0080200AC4CFF /* SecureIdAuthListContentNode.swift */; }; + D06CF82920D0119500AC4CFF /* SecureIdAuthListFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06CF82820D0119500AC4CFF /* SecureIdAuthListFieldNode.swift */; }; D06D37A92077DDF3009219B6 /* AutodownloadMediaCategoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */; }; D06D37B22077E77F009219B6 /* AutodownloadSizeLimitItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */; }; D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */; }; + D06ECFCB20B8448E00C576C2 /* ContactSynchronizationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06ECFCA20B8448E00C576C2 /* ContactSynchronizationSettings.swift */; }; D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */; }; D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */; }; D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */; }; @@ -182,6 +190,7 @@ D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */; }; D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */; }; D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; }; + D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */; }; D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */; }; D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */; }; D087BFB11F745483003FD209 /* ShareSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */; }; @@ -190,6 +199,8 @@ D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */; }; D08BDF641FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */; }; D08BDF661FA8CB10009D08E1 /* EditSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */; }; + D08D7E79209FA2930005D80C /* SecureIdValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08D7E78209FA2930005D80C /* SecureIdValues.swift */; }; + D08D7E8420A0F6020005D80C /* ExperimentalUISettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08D7E8320A0F6020005D80C /* ExperimentalUISettings.swift */; }; D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */; }; D091C7A61F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */; }; D09250041FE5363D003F693F /* ExperimentalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09250031FE5363D003F693F /* ExperimentalSettings.swift */; }; @@ -272,6 +283,11 @@ D0BE30452061C09000FBE6D8 /* SecureIdAuthContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE30442061C09000FBE6D8 /* SecureIdAuthContentNode.swift */; }; D0BE30472061C0BC00FBE6D8 /* SecureIdAuthPasswordOptionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE30462061C0BC00FBE6D8 /* SecureIdAuthPasswordOptionContentNode.swift */; }; D0BE30492061C0F500FBE6D8 /* SecureIdAuthHeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE30482061C0F500FBE6D8 /* SecureIdAuthHeaderNode.swift */; }; + D0BFAE4620AB04FB00793CF2 /* ChatRestrictedInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BFAE4520AB04FB00793CF2 /* ChatRestrictedInputPanelNode.swift */; }; + D0BFAE4E20AB1D7B00793CF2 /* DisabledContextResultsChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BFAE4D20AB1D7B00793CF2 /* DisabledContextResultsChatInputContextPanelNode.swift */; }; + D0BFAE5020AB2A1300793CF2 /* PeerBanTimeoutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BFAE4F20AB2A1300793CF2 /* PeerBanTimeoutController.swift */; }; + D0BFAE5B20AB35D200793CF2 /* IconSwitchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */; }; + D0BFAE5D20AB426300793CF2 /* PeerTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BFAE5C20AB426300793CF2 /* PeerTitle.swift */; }; D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */; }; D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */; }; D0C0B59B1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B59A1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift */; }; @@ -285,7 +301,11 @@ D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */; }; D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */; }; D0C44B641FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C44B631FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift */; }; - D0CA3F882073E4940042D2B6 /* SecureIdErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CA3F872073E4940042D2B6 /* SecureIdErrors.swift */; }; + D0CAD8FB20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CAD8FA20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift */; }; + D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CAD8FC20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift */; }; + D0CAD90120AEECAC00ACD96E /* ChatEditInterfaceMessageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CAD90020AEECAC00ACD96E /* ChatEditInterfaceMessageState.swift */; }; + D0CB27CF20C17A4A001ACF93 /* TermsOfServiceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB27CE20C17A4A001ACF93 /* TermsOfServiceController.swift */; }; + D0CB27D220C17A7F001ACF93 /* TermsOfServiceControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CB27D120C17A7F001ACF93 /* TermsOfServiceControllerNode.swift */; }; D0CE67941F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE67931F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift */; }; D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */; }; D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */; }; @@ -505,7 +525,6 @@ D0EC6CE81EB9F58800EBF1C3 /* DefaultPresentationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */; }; D0EC6CE91EB9F58800EBF1C3 /* DefaultDarkPresentationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174BF1EAE3AD400A1BF36 /* DefaultDarkPresentationTheme.swift */; }; D0EC6CEA1EB9F58800EBF1C3 /* DefaultPresentationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D010C2CD1EA7DDD600F41B96 /* DefaultPresentationStrings.swift */; }; - D0EC6CEB1EB9F58800EBF1C3 /* Wallpapers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05BFB601EAA27E200909D38 /* Wallpapers.swift */; }; D0EC6CEC1EB9F58800EBF1C3 /* PresentationThemeEssentialGraphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */; }; D0EC6CED1EB9F58800EBF1C3 /* StringPluralization.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EAE09F1EB21256005296C1 /* StringPluralization.swift */; }; D0EC6CEE1EB9F58800EBF1C3 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */; }; @@ -999,7 +1018,9 @@ D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeSettings.swift; sourceTree = ""; }; D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationTheme.swift; sourceTree = ""; }; D010C2CD1EA7DDD600F41B96 /* DefaultPresentationStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationStrings.swift; sourceTree = ""; }; + D0119CCF20CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySecureIdAttachmentMenu.swift; sourceTree = ""; }; D0127A0C1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPinnedMessageTitlePanelNode.swift; sourceTree = ""; }; + D013630B208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentGalleryFooterContentNode.swift; sourceTree = ""; }; D0147BA6206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdAuthAcceptNode.swift; sourceTree = ""; }; D0147BA8206EA35000E40378 /* SecureIdDocumentGalleryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentGalleryController.swift; sourceTree = ""; }; D0147BAA206EA6C100E40378 /* SecureIdDocumentImageGalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentImageGalleryItem.swift; sourceTree = ""; }; @@ -1057,6 +1078,7 @@ D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContactsManager.swift; sourceTree = ""; }; D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemTheme.swift; sourceTree = ""; }; D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = ""; }; + D01DBA9A209CC6AD00C64E64 /* ChatLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLinkPreview.swift; sourceTree = ""; }; D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputMediaRecordingButton.swift; sourceTree = ""; }; D0208AD31FA33D14001F0D5F /* RaiseToListenActivator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RaiseToListenActivator.h; sourceTree = ""; }; D0208AD41FA33D14001F0D5F /* RaiseToListenActivator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RaiseToListenActivator.m; sourceTree = ""; }; @@ -1167,6 +1189,8 @@ D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatItemGalleryFooterContentNode.swift; sourceTree = ""; }; D0430AFF1FF4570500A35ADD /* WebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebController.swift; sourceTree = ""; }; D0430B011FF4584100A35ADD /* WebControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebControllerNode.swift; sourceTree = ""; }; + D044A0F220BDA05800326FAC /* ThrottledValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottledValue.swift; sourceTree = ""; }; + D044A0FA20BDC40C00326FAC /* CachedChannelAdmins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedChannelAdmins.swift; sourceTree = ""; }; D046142D2004DB3700EC0EF2 /* LiveLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationManager.swift; sourceTree = ""; }; D04614362005094E00EC0EF2 /* DeviceLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLocationManager.swift; sourceTree = ""; }; D0461438200514F000EC0EF2 /* LiveLocationSummaryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationSummaryManager.swift; sourceTree = ""; }; @@ -1330,7 +1354,6 @@ D05B724C1E720393000BD3AD /* SelectivePrivacySettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectivePrivacySettingsController.swift; sourceTree = ""; }; D05B724F1E720597000BD3AD /* PresentationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationData.swift; sourceTree = ""; }; D05BFB5E1EAA22F900909D38 /* PresentationResourceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourceKey.swift; sourceTree = ""; }; - D05BFB601EAA27E200909D38 /* Wallpapers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wallpapers.swift; sourceTree = ""; }; D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoController.swift; sourceTree = ""; }; D0613FCC1E60482300202CDB /* ChannelMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersController.swift; sourceTree = ""; }; D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = ""; }; @@ -1344,10 +1367,13 @@ D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayUniversalVideoNode.swift; sourceTree = ""; }; D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayVideoDecoration.swift; sourceTree = ""; }; D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEmbedVideoContent.swift; sourceTree = ""; }; + D06CF82620D0080200AC4CFF /* SecureIdAuthListContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdAuthListContentNode.swift; sourceTree = ""; }; + D06CF82820D0119500AC4CFF /* SecureIdAuthListFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdAuthListFieldNode.swift; sourceTree = ""; }; D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadMediaCategoryController.swift; sourceTree = ""; }; D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadSizeLimitItem.swift; sourceTree = ""; }; D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLoadingNode.swift; sourceTree = ""; }; D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = ""; }; + D06ECFCA20B8448E00C576C2 /* ContactSynchronizationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSynchronizationSettings.swift; sourceTree = ""; }; D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = ""; }; D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeEssentialGraphics.swift; sourceTree = ""; }; D06FFBA91EAFAD2500CB53D4 /* PresentationResourcesChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChat.swift; sourceTree = ""; }; @@ -1395,6 +1421,7 @@ D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTitleProxyNode.swift; sourceTree = ""; }; D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyServerActionSheetController.swift; sourceTree = ""; }; D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = ""; }; + D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarGalleryItemFooterContentNode.swift; sourceTree = ""; }; D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = ""; }; D08774F91E3E2A5600A97350 /* ItemListCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListCheckboxItem.swift; sourceTree = ""; }; D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSoundStrings.swift; sourceTree = ""; }; @@ -1421,6 +1448,8 @@ D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/SwiftSignalKit.framework"; sourceTree = ""; }; D08D452D1D5E340300A7428A /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/TelegramCore.framework"; sourceTree = ""; }; + D08D7E78209FA2930005D80C /* SecureIdValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdValues.swift; sourceTree = ""; }; + D08D7E8320A0F6020005D80C /* ExperimentalUISettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalUISettings.swift; sourceTree = ""; }; D091C7A31F8EBB1E00D7DE13 /* ChatPresentationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPresentationData.swift; sourceTree = ""; }; D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsThemeWallpaperNode.swift; sourceTree = ""; }; D09250031FE5363D003F693F /* ExperimentalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettings.swift; sourceTree = ""; }; @@ -1542,6 +1571,11 @@ D0BE30482061C0F500FBE6D8 /* SecureIdAuthHeaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdAuthHeaderNode.swift; sourceTree = ""; }; D0BE383B1E7C3E51000079AF /* StickerPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewController.swift; sourceTree = ""; }; D0BE931A1E92DFBA00DCC1E6 /* StickerPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewControllerNode.swift; sourceTree = ""; }; + D0BFAE4520AB04FB00793CF2 /* ChatRestrictedInputPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRestrictedInputPanelNode.swift; sourceTree = ""; }; + D0BFAE4D20AB1D7B00793CF2 /* DisabledContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; + D0BFAE4F20AB2A1300793CF2 /* PeerBanTimeoutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerBanTimeoutController.swift; sourceTree = ""; }; + D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSwitchNode.swift; sourceTree = ""; }; + D0BFAE5C20AB426300793CF2 /* PeerTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerTitle.swift; sourceTree = ""; }; D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkHighlightingNode.swift; sourceTree = ""; }; D0C0B59A1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatSearchNavigationContentNode.swift; sourceTree = ""; }; @@ -1567,7 +1601,11 @@ D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatButtonKeyboardInputNode.swift; sourceTree = ""; }; D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBotInfoItem.swift; sourceTree = ""; }; D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = ""; }; - D0CA3F872073E4940042D2B6 /* SecureIdErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdErrors.swift; sourceTree = ""; }; + D0CAD8FA20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMemberCategoryListContext.swift; sourceTree = ""; }; + D0CAD8FC20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerChannelMemberCategoriesContextsManager.swift; sourceTree = ""; }; + D0CAD90020AEECAC00ACD96E /* ChatEditInterfaceMessageState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatEditInterfaceMessageState.swift; sourceTree = ""; }; + D0CB27CE20C17A4A001ACF93 /* TermsOfServiceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceController.swift; sourceTree = ""; }; + D0CB27D120C17A7F001ACF93 /* TermsOfServiceControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceControllerNode.swift; sourceTree = ""; }; D0CE1BD21E51BC6100404327 /* DebugController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugController.swift; sourceTree = ""; }; D0CE67931F7DB45100FFB557 /* ChatMessageContactBubbleContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContactBubbleContentNode.swift; sourceTree = ""; }; D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputAccessoryItem.swift; sourceTree = ""; }; @@ -2089,6 +2127,7 @@ D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */, D0FA34FE1EA5834C00E56FFA /* ItemListControllerSegmentedTitleView.swift */, D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */, + D0E6521D1E3A2305004EEA91 /* Items */, ); name = "Item List"; sourceTree = ""; @@ -2242,6 +2281,7 @@ D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */, D02383741DDF0E5E004018B6 /* ChatInterfaceTitlePanelNodes.swift */, D0DC35431DE32230000195EB /* ChatInterfaceStateContextQueries.swift */, + D0CAD90020AEECAC00ACD96E /* ChatEditInterfaceMessageState.swift */, ); name = "Interface State"; sourceTree = ""; @@ -2532,6 +2572,7 @@ isa = PBXGroup; children = ( D0575AF91EA0FDA7006F2541 /* AvatarGalleryController.swift */, + D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */, ); name = "Avatar Gallery"; sourceTree = ""; @@ -2631,6 +2672,7 @@ D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */, D07551921DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift */, D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */, + D0119CCF20CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift */, D023EBB11DDA800700BD496D /* LegacyMediaPickers.swift */, D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */, D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */, @@ -2731,7 +2773,9 @@ D09250031FE5363D003F693F /* ExperimentalSettings.swift */, D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */, D0B2F76120506E2A00D3BFB9 /* MediaInputSettings.swift */, + D08D7E8320A0F6020005D80C /* ExperimentalUISettings.swift */, D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */, + D06ECFCA20B8448E00C576C2 /* ContactSynchronizationSettings.swift */, ); name = Settings; sourceTree = ""; @@ -2814,6 +2858,7 @@ D093D7E12062F40100BC3599 /* SecureIdDocumentFormControllerNode.swift */, D0147BA8206EA35000E40378 /* SecureIdDocumentGalleryController.swift */, D0147BAA206EA6C100E40378 /* SecureIdDocumentImageGalleryItem.swift */, + D013630B208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift */, ); name = Documents; sourceTree = ""; @@ -2849,7 +2894,6 @@ D05174BF1EAE3AD400A1BF36 /* DefaultDarkPresentationTheme.swift */, D0A24D271F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift */, D010C2CD1EA7DDD600F41B96 /* DefaultPresentationStrings.swift */, - D05BFB601EAA27E200909D38 /* Wallpapers.swift */, D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */, D0EAE09F1EB21256005296C1 /* StringPluralization.swift */, ); @@ -2959,7 +3003,7 @@ D0C0B59E1EE082F5000F4D2C /* ChatSearchInputPanelNode.swift */, D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */, D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */, - D0D2686A1D788F6600C422DA /* Title Accessory Panels */, + D0BFAE4520AB04FB00793CF2 /* ChatRestrictedInputPanelNode.swift */, ); name = "Input Panels"; sourceTree = ""; @@ -3005,7 +3049,7 @@ D0BE30462061C0BC00FBE6D8 /* SecureIdAuthPasswordOptionContentNode.swift */, D093D7DA2062CFF500BC3599 /* SecureIdAuthFormContentNode.swift */, D093D7DC2062D09A00BC3599 /* SecureIdAuthFormFieldNode.swift */, - D0CA3F872073E4940042D2B6 /* SecureIdErrors.swift */, + D08D7E78209FA2930005D80C /* SecureIdValues.swift */, D093D7E02062F3F400BC3599 /* Documents */, D02D60AF206C188000FEFE1E /* Plaintext Fields */, D093D81C206994FD00BC3599 /* FindSecureIdValue.swift */, @@ -3014,6 +3058,8 @@ D02D60C7206E705D00FEFE1E /* SecureIdValueFormPhoneItem.swift */, D0E412DE206AA00500BEE4A2 /* SecureIdVerificationDocumentsContext.swift */, D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */, + D06CF82620D0080200AC4CFF /* SecureIdAuthListContentNode.swift */, + D06CF82820D0119500AC4CFF /* SecureIdAuthListFieldNode.swift */, ); name = "Secure ID"; sourceTree = ""; @@ -3057,6 +3103,15 @@ name = "Data and Storage"; sourceTree = ""; }; + D0CB27D020C17A6D001ACF93 /* Terms of Service */ = { + isa = PBXGroup; + children = ( + D0CB27CE20C17A4A001ACF93 /* TermsOfServiceController.swift */, + D0CB27D120C17A7F001ACF93 /* TermsOfServiceControllerNode.swift */, + ); + name = "Terms of Service"; + sourceTree = ""; + }; D0CE8CEA1F6FCC8200AA2DB0 /* Transform Image */ = { isa = PBXGroup; children = ( @@ -3254,6 +3309,7 @@ children = ( D0E35A051DE4801600BC6096 /* Vertical List */, D099EA1D1DE744EE001AF5A8 /* Horizontal List */, + D0BFAE4D20AB1D7B00793CF2 /* DisabledContextResultsChatInputContextPanelNode.swift */, ); name = "Context Request Results"; sourceTree = ""; @@ -3300,6 +3356,7 @@ D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */, D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */, + D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */, ); name = Items; sourceTree = ""; @@ -3489,6 +3546,7 @@ D0E8B8BA2044780600605593 /* ItemListSecretChatKeyItem.swift */, D0E8B8BC204479A500605593 /* SecretChatKeyController.swift */, D0E8B8BE20447A4600605593 /* SecretChatKeyControllerNode.swift */, + D0BFAE4F20AB2A1300793CF2 /* PeerBanTimeoutController.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -3755,7 +3813,9 @@ D021E0CC1DB4132E00C6B04F /* Input Nodes */, D0DF0C961D81FD87008AEB01 /* Input Context Panels */, D0BA6F811D784C3A0034826E /* Input Panels */, + D0D2686A1D788F6600C422DA /* Title Accessory Panels */, D0F69E441D6B8B850046BCD6 /* History Navigation */, + D044A0FA20BDC40C00326FAC /* CachedChannelAdmins.swift */, ); name = Chat; sourceTree = ""; @@ -3894,7 +3954,6 @@ D0F69E671D6B8C030046BCD6 /* Map Input */ = { isa = PBXGroup; children = ( - D0E6521D1E3A2305004EEA91 /* Items */, D0F69E681D6B8C160046BCD6 /* MapInputController.swift */, D0F69E691D6B8C160046BCD6 /* MapInputControllerNode.swift */, ); @@ -3925,6 +3984,7 @@ D0FA0AC31E7742EE005BB9B7 /* Stickers */, D05BFB4F1EA96EC100909D38 /* Themes */, D0AF7C441ED84BB000CD8E0F /* Language Selection */, + D0CB27D020C17A6D001ACF93 /* Terms of Service */, D01B279A1E39386C0022A4C0 /* SettingsController.swift */, D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */, D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, @@ -4019,6 +4079,11 @@ D0DE5804205B202500C356A8 /* ScreenCaptureDetection.swift */, D0BE3036206139F500FBE6D8 /* ImageCompression.swift */, D00781042084DFB100369A39 /* UrlEscaping.swift */, + D01DBA9A209CC6AD00C64E64 /* ChatLinkPreview.swift */, + D0BFAE5C20AB426300793CF2 /* PeerTitle.swift */, + D0CAD8FA20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift */, + D0CAD8FC20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift */, + D044A0F220BDA05800326FAC /* ThrottledValue.swift */, ); name = Utils; sourceTree = ""; @@ -4349,11 +4414,13 @@ D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */, D0EC6CAE1EB9F58800EBF1C3 /* animations.c in Sources */, D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */, + D0119CD020CAE75F00895300 /* LegacySecureIdAttachmentMenu.swift in Sources */, D0EC6CAF1EB9F58800EBF1C3 /* buffer.c in Sources */, D0EC6CB01EB9F58800EBF1C3 /* objects.c in Sources */, D0EC6CB11EB9F58800EBF1C3 /* program.c in Sources */, D0E412DA206A894800BEE4A2 /* SecureIdValueFormFileItem.swift in Sources */, D0EC6CB21EB9F58800EBF1C3 /* rngs.c in Sources */, + D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */, D0EC6CB31EB9F58800EBF1C3 /* shader.c in Sources */, D0F0AAE21EC20EF8005EE2A5 /* CallControllerStatusNode.swift in Sources */, D0EC6CB41EB9F58800EBF1C3 /* timing.c in Sources */, @@ -4391,6 +4458,7 @@ D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */, D0477D1F1F619E0700412B44 /* GalleryVideoDecoration.swift in Sources */, D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */, + D0CAD8FB20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift in Sources */, D0EC6CC21EB9F58800EBF1C3 /* LegacyEmptyController.swift in Sources */, D0EC6CC31EB9F58800EBF1C3 /* LegacyNavigationController.swift in Sources */, D0EC6CC41EB9F58800EBF1C3 /* LegacyLocationPicker.swift in Sources */, @@ -4462,7 +4530,6 @@ D0EC6CE91EB9F58800EBF1C3 /* DefaultDarkPresentationTheme.swift in Sources */, D0EC6CEA1EB9F58800EBF1C3 /* DefaultPresentationStrings.swift in Sources */, D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */, - D0EC6CEB1EB9F58800EBF1C3 /* Wallpapers.swift in Sources */, D0EC6CEC1EB9F58800EBF1C3 /* PresentationThemeEssentialGraphics.swift in Sources */, D01BAA1E1ECC931D00295217 /* CallListNodeEntries.swift in Sources */, D0EC6CED1EB9F58800EBF1C3 /* StringPluralization.swift in Sources */, @@ -4514,6 +4581,7 @@ D0185E8A208A01AF005E1A6C /* ProxySettingsActionItem.swift in Sources */, D0EC6D051EB9F58800EBF1C3 /* picture.c in Sources */, D0EC6D061EB9F58800EBF1C3 /* wav_io.c in Sources */, + D06ECFCB20B8448E00C576C2 /* ContactSynchronizationSettings.swift in Sources */, D0EC6D071EB9F58800EBF1C3 /* bitwise.c in Sources */, D0EC6D081EB9F58800EBF1C3 /* framing.c in Sources */, D0EC6D091EB9F58800EBF1C3 /* info.c in Sources */, @@ -4595,9 +4663,11 @@ D0EC6D311EB9F58800EBF1C3 /* RadialTimeoutNode.swift in Sources */, D0EC6D321EB9F58800EBF1C3 /* TextNode.swift in Sources */, D0EC6D331EB9F58800EBF1C3 /* ListSectionHeaderNode.swift in Sources */, + D0BFAE5020AB2A1300793CF2 /* PeerBanTimeoutController.swift in Sources */, D0BDB09B1F79C658002ABF2F /* SaveToCameraRoll.swift in Sources */, D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */, D0EC6D341EB9F58800EBF1C3 /* AvatarNode.swift in Sources */, + D08D7E8420A0F6020005D80C /* ExperimentalUISettings.swift in Sources */, D0EC6D351EB9F58800EBF1C3 /* SearchBarNode.swift in Sources */, D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, D0E8B8B9204477B600605593 /* SecretChatKeyVisualization.swift in Sources */, @@ -4620,7 +4690,6 @@ D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */, D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */, D0EC6D3F1EB9F58800EBF1C3 /* MediaNavigationAccessoryPanel.swift in Sources */, - D0CA3F882073E4940042D2B6 /* SecureIdErrors.swift in Sources */, D0E9BA3B1F0558E800F079A4 /* NSString+Stripe.m in Sources */, D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */, D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */, @@ -4652,6 +4721,7 @@ D0EC6D581EB9F58800EBF1C3 /* ChatHistoryGridNode.swift in Sources */, D0B2F76E2052B59F00D3BFB9 /* InviteContactsController.swift in Sources */, D0EC6D591EB9F58800EBF1C3 /* ChatMessageThrottledProcessingManager.swift in Sources */, + D0BFAE4620AB04FB00793CF2 /* ChatRestrictedInputPanelNode.swift in Sources */, D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */, D0EC6D5A1EB9F58800EBF1C3 /* ListMessageItem.swift in Sources */, D0EC6D5B1EB9F58800EBF1C3 /* ListMessageNode.swift in Sources */, @@ -4681,6 +4751,7 @@ D0EC6D6A1EB9F58800EBF1C3 /* AuthorizationSequenceSplashControllerNode.swift in Sources */, D0EC6D6B1EB9F58800EBF1C3 /* AuthorizationSequenceCountrySelectionController.swift in Sources */, D0EC6D6C1EB9F58800EBF1C3 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */, + D0BFAE5D20AB426300793CF2 /* PeerTitle.swift in Sources */, D0EC6D6D1EB9F58800EBF1C3 /* AuthorizationSequencePhoneEntryController.swift in Sources */, D0EC6D6E1EB9F58800EBF1C3 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */, D0B85C211FF70BEC00E795B4 /* AuthorizationSequenceAwaitingAccountResetControllerNode.swift in Sources */, @@ -4741,6 +4812,8 @@ D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */, D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */, + D01DBA9B209CC6AD00C64E64 /* ChatLinkPreview.swift in Sources */, + D044A0FB20BDC40C00326FAC /* CachedChannelAdmins.swift in Sources */, D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */, D0EC6D911EB9F58900EBF1C3 /* ChatMessageBubbleItemNode.swift in Sources */, D0E8B8BD204479A500605593 /* SecretChatKeyController.swift in Sources */, @@ -4760,10 +4833,13 @@ D0EC6D961EB9F58900EBF1C3 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0B2F7722052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift in Sources */, D0EC6D971EB9F58900EBF1C3 /* ChatMessageItem.swift in Sources */, + D044A0F320BDA05800326FAC /* ThrottledValue.swift in Sources */, + D08D7E79209FA2930005D80C /* SecureIdValues.swift in Sources */, D0E8175720122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift in Sources */, D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */, D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, + D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, D0430B001FF4570500A35ADD /* WebController.swift in Sources */, D0EC6D991EB9F58900EBF1C3 /* ChatMessageMediaBubbleContentNode.swift in Sources */, @@ -4775,6 +4851,7 @@ D0E9BA2B1F0557A600F079A4 /* STPFormEncoder.m in Sources */, D01BAA1C1ECC92F700295217 /* CallListViewTransition.swift in Sources */, D0EC6D9E1EB9F58900EBF1C3 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, + D06CF82720D0080200AC4CFF /* SecureIdAuthListContentNode.swift in Sources */, D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */, D0EC6D9F1EB9F58900EBF1C3 /* ChatUnreadItem.swift in Sources */, D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */, @@ -4804,6 +4881,7 @@ D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */, D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */, D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */, + D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */, D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */, D0EC6DAD1EB9F58900EBF1C3 /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0EC6DAE1EB9F58900EBF1C3 /* ChatInterfaceStateContextMenus.swift in Sources */, @@ -4852,6 +4930,7 @@ D0430B021FF4584100A35ADD /* WebControllerNode.swift in Sources */, D0EC6DCC1EB9F58900EBF1C3 /* ChatButtonKeyboardInputNode.swift in Sources */, D0CFBB861FD715E700B65C0D /* LegacyHTTPOperationImpl.swift in Sources */, + D06CF82920D0119500AC4CFF /* SecureIdAuthListFieldNode.swift in Sources */, D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */, D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */, D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */, @@ -4870,6 +4949,7 @@ D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */, D0B2F76220506E2A00D3BFB9 /* MediaInputSettings.swift in Sources */, + D0BFAE4E20AB1D7B00793CF2 /* DisabledContextResultsChatInputContextPanelNode.swift in Sources */, D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */, D020A9DA1FEAE675008C66F7 /* OverlayPlayerController.swift in Sources */, D0E817472010E62F00B82BBB /* MergeLists.swift in Sources */, @@ -4888,6 +4968,7 @@ D0EC6DDE1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */, D0EC6DDF1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, + D0BFAE5B20AB35D200793CF2 /* IconSwitchNode.swift in Sources */, D0EC6DE01EB9F58900EBF1C3 /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, D09D88711F86D36700BEB4C9 /* CountryList.swift in Sources */, D0EC6DE11EB9F58900EBF1C3 /* ChatMessageSelectionInputPanelNode.swift in Sources */, @@ -4895,6 +4976,7 @@ D0EC6DE21EB9F58900EBF1C3 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, D0EC6DE31EB9F58900EBF1C3 /* ChatBotStartInputPanelNode.swift in Sources */, D0EC6DE41EB9F58900EBF1C3 /* ChatUnblockInputPanelNode.swift in Sources */, + D0CAD90120AEECAC00ACD96E /* ChatEditInterfaceMessageState.swift in Sources */, D0EC6DE51EB9F58900EBF1C3 /* SecretChatHandshakeStatusInputPanelNode.swift in Sources */, D06BEC771F62F68B0035A545 /* OverlayUniversalVideoNode.swift in Sources */, D0EC6DE61EB9F58900EBF1C3 /* DeleteChatInputPanelNode.swift in Sources */, @@ -5062,6 +5144,7 @@ D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */, D0A24D281F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift in Sources */, D025A4231F79344500563950 /* FetchManager.swift in Sources */, + D0CB27CF20C17A4A001ACF93 /* TermsOfServiceController.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, D0EC6E511EB9F58900EBF1C3 /* ChannelBlacklistController.swift in Sources */, @@ -5145,6 +5228,7 @@ D053DADA201A4C4400993D32 /* ChatTextInputAttributes.swift in Sources */, D0EC6E821EB9F58900EBF1C3 /* NotificationContainerControllerNode.swift in Sources */, D0EC6E831EB9F58900EBF1C3 /* NotificationItemContainerNode.swift in Sources */, + D0CB27D220C17A7F001ACF93 /* TermsOfServiceControllerNode.swift in Sources */, D0EC6E841EB9F58900EBF1C3 /* NotificationItem.swift in Sources */, D0EC6E851EB9F58900EBF1C3 /* ChatMessageNotificationItem.swift in Sources */, D0EC6E861EB9F58900EBF1C3 /* UIImage+WebP.m in Sources */, diff --git a/TelegramUI/AccessoryPanelNode.swift b/TelegramUI/AccessoryPanelNode.swift index 1a41aa8d9d..b56f6790b2 100644 --- a/TelegramUI/AccessoryPanelNode.swift +++ b/TelegramUI/AccessoryPanelNode.swift @@ -7,4 +7,7 @@ class AccessoryPanelNode: ASDisplayNode { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { } + + func updateState(size: CGSize, interfaceState: ChatPresentationInterfaceState) { + } } diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 07beba5ac3..cdb3ed583a 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -245,8 +245,10 @@ public func archivedStickerPacksController(account: Account) -> ViewController { let installedStickerPacks = Promise() installedStickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + let arguments = ArchivedStickerPacksControllerArguments(account: account, openStickerPack: { info in - presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentStickerPackController?(info) }, setPackIdWithRevealedOptions: { packId, fromPackId in updateState { state in if (packId == nil && fromPackId == state.packIdWithRevealedOptions) || (packId != nil && fromPackId == nil) { @@ -339,5 +341,9 @@ public func archivedStickerPacksController(account: Account) -> ViewController { } } + presentStickerPackController = { [weak controller] info in + presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + return controller } diff --git a/TelegramUI/AuthorizationSequenceCodeEntryController.swift b/TelegramUI/AuthorizationSequenceCodeEntryController.swift index 3e174c1c68..8e86e276a3 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryController.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryController.swift @@ -10,11 +10,14 @@ final class AuthorizationSequenceCodeEntryController: ViewController { private let strings: PresentationStrings private let theme: AuthorizationTheme + private let openUrl: (String) -> Void var loginWithCode: ((String) -> Void)? + var reset: (() -> Void)? var requestNextOption: (() -> Void)? var data: (String, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?)? + var termsOfService: UnauthorizedAccountTermsOfService? private let hapticFeedback = HapticFeedback() @@ -30,9 +33,10 @@ final class AuthorizationSequenceCodeEntryController: ViewController { } } - init(strings: PresentationStrings, theme: AuthorizationTheme) { + init(strings: PresentationStrings, theme: AuthorizationTheme, openUrl: @escaping (String) -> Void) { self.strings = strings self.theme = theme + self.openUrl = openUrl super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) @@ -52,7 +56,7 @@ final class AuthorizationSequenceCodeEntryController: ViewController { self.displayNodeDidLoad() self.controllerNode.loginWithCode = { [weak self] code in - self?.loginWithCode?(code) + self?.continueWithCode(code) } self.controllerNode.requestNextOption = { [weak self] in @@ -74,7 +78,8 @@ final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.activateInput() } - func updateData(number: String, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) { + func updateData(number: String, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: UnauthorizedAccountTermsOfService?) { + self.termsOfService = termsOfService if self.data?.0 != number || self.data?.1 != codeType || self.data?.2 != nextType || self.data?.3 != timeout { self.data = (number, codeType, nextType, timeout) if self.isNodeLoaded { @@ -95,7 +100,35 @@ final class AuthorizationSequenceCodeEntryController: ViewController { hapticFeedback.error() self.controllerNode.animateError() } else { - self.loginWithCode?(self.controllerNode.currentCode) + self.continueWithCode(self.controllerNode.currentCode) + } + } + + private func continueWithCode(_ code: String) { + if let termsOfService = self.termsOfService { + var acceptImpl: (() -> Void)? + var declineImpl: (() -> Void)? + let controller = TermsOfServiceController(theme: defaultDarkPresentationTheme, strings: self.strings, text: termsOfService.text, entities: termsOfService.entities, ageConfirmation: termsOfService.ageConfirmation, signingUp: true, accept: { + acceptImpl?() + }, decline: { + declineImpl?() + }, openUrl: { [weak self] url in + self?.openUrl(url) + }) + acceptImpl = { [weak self, weak controller] in + controller?.dismiss() + if let strongSelf = self { + strongSelf.termsOfService = nil + strongSelf.loginWithCode?(code) + } + } + declineImpl = { [weak self, weak controller] in + controller?.dismiss() + self?.reset?() + } + self.present(controller, in: .window(.root)) + } else { + self.loginWithCode?(code) } } } diff --git a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift index a8a2807618..bf68f7368d 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift @@ -141,6 +141,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeField.textField.font = Font.regular(24.0) self.codeField.textField.textAlignment = .center self.codeField.textField.keyboardType = .numberPad + #if swift(>=4.2) + if #available(iOSApplicationExtension 12.0, *) { + self.codeField.textField.textContentType = .oneTimeCode + } + #endif self.codeField.textField.returnKeyType = .done self.codeField.textField.textColor = self.theme.primaryColor self.codeField.textField.keyboardAppearance = self.theme.keyboardAppearance diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index ba2e153556..6ec1830ed2 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -18,15 +18,17 @@ public final class AuthorizationSequenceController: NavigationController { private let apiHash: String private let strings: PresentationStrings public let theme: AuthorizationTheme + private let openUrl: (String) -> Void private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() - public init(account: UnauthorizedAccount, strings: PresentationStrings, apiId: Int32, apiHash: String) { + public init(account: UnauthorizedAccount, strings: PresentationStrings, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String) { self.account = account self.apiId = apiId self.apiHash = apiHash self.strings = strings + self.openUrl = openUrl self.theme = defaultAuthorizationTheme super.init(mode: .single, theme: NavigationControllerTheme(navigationBar: AuthorizationSequenceController.navigationBarTheme(theme), emptyAreaColor: .black, emptyDetailIcon: nil)) @@ -84,8 +86,8 @@ public final class AuthorizationSequenceController: NavigationController { } } - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - modifier.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + transaction.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() } } @@ -105,7 +107,9 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePhoneEntryController(strings: self.strings, theme: self.theme) + controller = AuthorizationSequencePhoneEntryController(network: self.account.network, strings: self.strings, theme: self.theme, openUrl: { [weak self] url in + self?.openUrl(url) + }) controller.loginWithNumber = { [weak self, weak controller] number in if let strongSelf = self { controller?.inProgress = true @@ -142,7 +146,7 @@ public final class AuthorizationSequenceController: NavigationController { return controller } - private func codeEntryController(number: String, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) -> AuthorizationSequenceCodeEntryController { + private func codeEntryController(number: String, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: UnauthorizedAccountTermsOfService?) -> AuthorizationSequenceCodeEntryController { var currentController: AuthorizationSequenceCodeEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceCodeEntryController { @@ -154,7 +158,9 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceCodeEntryController(strings: self.strings, theme: self.theme) + controller = AuthorizationSequenceCodeEntryController(strings: self.strings, theme: self.theme, openUrl: { [weak self] url in + self?.openUrl(url) + }) controller.loginWithCode = { [weak self, weak controller] code in if let strongSelf = self { controller?.inProgress = true @@ -226,7 +232,15 @@ public final class AuthorizationSequenceController: NavigationController { } } } - controller.updateData(number: formatPhoneNumber(number), codeType: type, nextType: nextType, timeout: timeout) + controller.reset = { [weak self] in + if let strongSelf = self { + let account = strongSelf.account + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + transaction.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty)) + }).start() + } + } + controller.updateData(number: formatPhoneNumber(number), codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService) return controller } @@ -263,6 +277,7 @@ public final class AuthorizationSequenceController: NavigationController { } controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.passwordIsInvalid() } } })) @@ -278,9 +293,9 @@ public final class AuthorizationSequenceController: NavigationController { strongController.inProgress = false switch option { case let .email(pattern): - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - if let state = modifier.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code) = state.contents { - modifier.setState(UnauthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern))) + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code) = state.contents { + transaction.setState(UnauthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern))) } }).start() case .none: @@ -368,9 +383,9 @@ public final class AuthorizationSequenceController: NavigationController { if let strongSelf = self, let controller = controller { controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) let account = strongSelf.account - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - if let state = modifier.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _) = state.contents { - modifier.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code))) + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _) = state.contents { + transaction.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code))) } }).start() } @@ -423,8 +438,8 @@ public final class AuthorizationSequenceController: NavigationController { controller.logout = { [weak self] in if let strongSelf = self { let account = strongSelf.account - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - modifier.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty)) + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + transaction.setState(UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() } } @@ -490,8 +505,8 @@ public final class AuthorizationSequenceController: NavigationController { } case let .phoneEntry(countryCode, number): self.setViewControllers([self.splashController(), self.phoneEntryController(countryCode: countryCode, number: number)], animated: !self.viewControllers.isEmpty) - case let .confirmationCodeEntry(number, type, _, timeout, nextType): - self.setViewControllers([self.splashController(), self.codeEntryController(number: number, type: type, nextType: nextType, timeout: timeout)], animated: !self.viewControllers.isEmpty) + case let .confirmationCodeEntry(number, type, _, timeout, nextType, termsOfService): + self.setViewControllers([self.splashController(), self.codeEntryController(number: number, type: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService)], animated: !self.viewControllers.isEmpty) case let .passwordEntry(hint, _, _): self.setViewControllers([self.splashController(), self.passwordEntryController(hint: hint)], animated: !self.viewControllers.isEmpty) case let .passwordRecovery(_, _, _, emailPattern): diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 560d405603..65556b54ab 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -51,21 +51,9 @@ private func loadCountryCodes() -> [(String, Int)] { private let countryCodes: [(String, Int)] = loadCountryCodes() -func localizedContryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] { - let locale = localeWithStrings(strings) - var result: [((String, String), String, Int)] = [] - for (id, code) in countryCodes { - if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: id), let countryName = locale.localizedString(forRegionCode: id) { - result.append(((englishCountryName, countryName), id, code)) - } else { - assertionFailure() - } - } - return result -} - private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { private let displayCodes: Bool + private let needsSubtitle: Bool private let theme: AuthorizationTheme private let tableView: UITableView @@ -78,9 +66,10 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl var itemSelected: ((((String, String), String, Int)) -> Void)? - init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { + init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool, needsSubtitle: Bool) { self.displayCodes = displayCodes self.theme = theme + self.needsSubtitle = needsSubtitle self.tableView = UITableView(frame: CGRect(), style: .plain) @@ -116,7 +105,7 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { cell = currentCell } else { - cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CountryCell") + cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label @@ -145,6 +134,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi private let strings: PresentationStrings private let theme: AuthorizationTheme private let displayCodes: Bool + private let needsSubtitle: Bool private let tableView: UITableView @@ -161,6 +151,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi self.strings = strings self.theme = theme self.displayCodes = displayCodes + self.needsSubtitle = strings.languageCode != "en" self.tableView = UITableView(frame: CGRect(), style: .plain) @@ -198,7 +189,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi self.view.backgroundColor = .white - self.searchResultsController = InnerCoutrySearchResultsController(strings: self.strings, theme: self.theme, displayCodes: self.displayCodes) + self.searchResultsController = InnerCoutrySearchResultsController(strings: self.strings, theme: self.theme, displayCodes: self.displayCodes, needsSubtitle: self.needsSubtitle) self.searchResultsController.itemSelected = { [weak self] item in self?.itemSelected?(item) } @@ -291,7 +282,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { cell = currentCell } else { - cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CountryCell") + cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label @@ -337,6 +328,65 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi } } +private final class AuthorizationSequenceCountrySelectionNavigationContentNode: NavigationBarContentNode { + private let theme: AuthorizationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + private let searchBar: SearchBarNode + + private var queryUpdated: ((String) -> Void)? + + init(theme: AuthorizationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: defaultDarkPresentationTheme, strings: strings) + let placeholderText = strings.Common_Search + let searchBarFont = Font.regular(14.0) + + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.searchBarTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query in + self?.queryUpdated?(query) + } + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let searchBarFrame = CGRect(origin: CGPoint(), size: size) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} + final class AuthorizationSequenceCountrySelectionController: ViewController { static func lookupCountryNameById(_ id: String, strings: PresentationStrings) -> String? { for (itemId, _) in countryCodes { @@ -361,38 +411,39 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { } private let theme: AuthorizationTheme + private let strings: PresentationStrings + private let displayCodes: Bool + + private var navigationContentNode: AuthorizationSequenceCountrySelectionNavigationContentNode? private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode { return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode } - private let innerNavigationController: UINavigationController - private let innerController: InnerCountrySelectionController - var completeWithCountryCode: ((Int, String) -> Void)? + var dismissed: (() -> Void)? init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool = true) { self.theme = theme - self.innerController = InnerCountrySelectionController(strings: strings, theme: theme, displayCodes: displayCodes) - self.innerNavigationController = UINavigationController(rootViewController: self.innerController) - self.innerController.navigation_setNavigationController(self.innerNavigationController) - self.innerNavigationController.navigationBar.barTintColor = theme.navigationBarBackgroundColor - self.innerNavigationController.navigationBar.tintColor = theme.accentColor - self.innerNavigationController.navigationBar.shadowImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.navigationBarSeparatorColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: UIScreenPixel))) - }) - self.innerNavigationController.navigationBar.isTranslucent = false - self.innerNavigationController.navigationBar.titleTextAttributes = [NSAttributedStringKey.font: Font.semibold(17.0), NSAttributedStringKey.foregroundColor: theme.navigationBarTextColor] + self.strings = strings + self.displayCodes = displayCodes - super.init(navigationBarPresentationData: nil) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) self.statusBar.statusBarStyle = theme.statusBarStyle - self.innerController.dismiss = { [weak self] in - self?.cancelPressed() + let navigationContentNode = AuthorizationSequenceCountrySelectionNavigationContentNode(theme: theme, strings: strings, cancel: { [weak self] in + self?.dismissed?() + self?.dismiss() + }) + self.navigationContentNode = navigationContentNode + navigationContentNode.setQueryUpdated { [weak self] query in + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return + } + strongSelf.controllerNode.updateSearchQuery(query) } + self.navigationBar?.setContentNode(navigationContentNode, animated: false) } required init(coder aDecoder: NSCoder) { @@ -400,44 +451,35 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequenceCountrySelectionControllerNode() - self.displayNodeDidLoad() - - self.innerNavigationController.willMove(toParentViewController: self) - self.addChildViewController(self.innerNavigationController) - self.displayNode.view.addSubview(self.innerNavigationController.view) - self.innerNavigationController.didMove(toParentViewController: self) - - self.innerController.itemSelected = { [weak self] args in + self.displayNode = AuthorizationSequenceCountrySelectionControllerNode(theme: self.theme, strings: self.strings, displayCodes: self.displayCodes, itemSelected: { [weak self] args in let (_, countryId, code) = args self?.completeWithCountryCode?(code, countryId) - self?.controllerNode.animateOut() - } - - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: true, completion: nil) - } + self?.dismiss() + }) + self.displayNodeDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.innerNavigationController.viewWillAppear(false) - self.innerNavigationController.viewDidAppear(false) - self.controllerNode.animateIn() + self.navigationContentNode?.activate() } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - transition.animateView { - self.innerNavigationController.view.frame = CGRect(origin: CGPoint(), size: layout.size) - //self.innerController.view.frame = CGRect(origin: CGPoint(), size: layout.size) - } + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } private func cancelPressed() { - self.controllerNode.animateOut() + self.dismissed?() + self.dismiss(completion: nil) + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: true, completion: nil) + }) } } diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift index 30eed41f06..b85791435a 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -3,31 +3,276 @@ import AsyncDisplayKit import Display import TelegramCore -final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode { - var dismiss: (() -> Void)? +private func loadCountryCodes() -> [(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 [] + } - override init() { + let delimiter = ";" + let endOfLine = "\n" + + var result: [(String, Int)] = [] + + var currentLocation = data.startIndex + + while true { + guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex) else { + break + } + + let countryCode = String(data[currentLocation ..< codeRange.lowerBound]) + + guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else { + break + } + + let countryId = String(data[codeRange.upperBound ..< idRange.lowerBound]) + + let maybeNameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) + + if let countryCodeInt = Int(countryCode) { + result.append((countryId, countryCodeInt)) + } + + if let maybeNameRange = maybeNameRange { + currentLocation = maybeNameRange.upperBound + } else { + break + } + } + + return result +} + +private let countryCodes: [(String, Int)] = loadCountryCodes() + +func localizedContryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] { + let locale = localeWithStrings(strings) + var result: [((String, String), String, Int)] = [] + for (id, code) in countryCodes { + if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: id), let countryName = locale.localizedString(forRegionCode: id) { + result.append(((englishCountryName, countryName), id, code)) + } else { + assertionFailure() + } + } + return result +} + +final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, UITableViewDelegate, UITableViewDataSource { + let itemSelected: (((String, String), String, Int)) -> Void + + private let theme: AuthorizationTheme + private let strings: PresentationStrings + private let displayCodes: Bool + private let needsSubtitle: Bool + + private let tableView: UITableView + private let searchTableView: UITableView + + private let sections: [(String, [((String, String), String, Int)])] + private let sectionTitles: [String] + + private var searchResults: [((String, String), String, Int)] = [] + + init(theme: AuthorizationTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) { + self.theme = theme + self.strings = strings + self.displayCodes = displayCodes + self.itemSelected = itemSelected + + self.needsSubtitle = strings.languageCode != "en" + + self.tableView = UITableView(frame: CGRect(), style: .plain) + self.searchTableView = UITableView(frame: CGRect(), style: .plain) + self.searchTableView.isHidden = true + + let countryNamesAndCodes = localizedContryNamesAndCodes(strings: strings) + + var sections: [(String, [((String, String), String, Int)])] = [] + for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in + return lhs.0 < rhs.0 + }) { + let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased() + if sections.isEmpty || sections[sections.count - 1].0 != title { + sections.append((title, [])) + } + sections[sections.count - 1].1.append((names, id, code)) + } + self.sections = sections + var sectionTitles = sections.map { $0.0 } + sectionTitles.insert(UITableViewIndexSearch, at: 0) + self.sectionTitles = sectionTitles + super.init() self.setViewBlock({ return UITracingLayerView() }) - self.backgroundColor = UIColor.white + self.backgroundColor = theme.backgroundColor + + self.tableView.backgroundColor = theme.backgroundColor + + self.tableView.backgroundColor = self.theme.backgroundColor + self.tableView.separatorColor = self.theme.separatorColor + self.tableView.backgroundView = UIView() + self.tableView.sectionIndexColor = self.theme.accentColor + + self.searchTableView.backgroundColor = theme.backgroundColor + + self.searchTableView.backgroundColor = self.theme.backgroundColor + self.searchTableView.separatorColor = self.theme.separatorColor + self.searchTableView.backgroundView = UIView() + self.searchTableView.sectionIndexColor = self.theme.accentColor + + self.tableView.delegate = self + self.tableView.dataSource = self + + self.searchTableView.delegate = self + self.searchTableView.dataSource = self + + self.view.addSubview(self.tableView) + self.view.addSubview(self.searchTableView) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: self.tableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) + transition.updateFrame(view: self.searchTableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: 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() { - 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?() - } + func animateOut(completion: @escaping () -> Void) { + 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() }) } + + func updateSearchQuery(_ query: String) { + if query.isEmpty { + self.searchResults = [] + self.searchTableView.reloadData() + self.searchTableView.isHidden = true + } else { + let normalizedQuery = query.lowercased().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + var results: [((String, String), String, Int)] = [] + for (_, items) in self.sections { + for item in items { + if item.0.0.lowercased().hasPrefix(normalizedQuery) || item.0.1.lowercased().hasPrefix(normalizedQuery) { + results.append(item) + } + } + } + + self.searchResults = results + self.searchTableView.isHidden = false + self.searchTableView.reloadData() + } + } + + func numberOfSections(in tableView: UITableView) -> Int { + if tableView === self.tableView { + return self.sections.count + } else { + return 1 + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if tableView === self.tableView { + return self.sections[section].1.count + } else { + return self.searchResults.count + } + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if tableView === self.tableView { + return self.sections[section].0 + } else { + return nil + } + } + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.backgroundColor + (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.primaryColor + } + + func sectionIndexTitles(for tableView: UITableView) -> [String]? { + if tableView === self.tableView { + return self.sectionTitles + } else { + return nil + } + } + + func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + if tableView === self.tableView { + if index == 0 { + return 0 + } else { + return max(0, index - 1) + } + } else { + return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { + cell = currentCell + } else { + cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") + let label = UILabel() + label.font = Font.medium(17.0) + cell.accessoryView = label + cell.selectedBackgroundView = UIView() + } + + let countryName: String + let originalCountryName: String + let code: String + if tableView === self.tableView { + countryName = self.sections[indexPath.section].1[indexPath.row].0.1 + originalCountryName = self.sections[indexPath.section].1[indexPath.row].0.0 + code = "+\(self.sections[indexPath.section].1[indexPath.row].2)" + } else { + countryName = self.searchResults[indexPath.row].0.1 + originalCountryName = self.searchResults[indexPath.row].0.0 + code = "+\(self.searchResults[indexPath.row].2)" + } + + cell.textLabel?.text = countryName + cell.detailTextLabel?.text = originalCountryName + if self.displayCodes, let label = cell.accessoryView as? UILabel { + label.text = code + label.sizeToFit() + label.textColor = self.theme.primaryColor + } + cell.textLabel?.textColor = self.theme.primaryColor + cell.detailTextLabel?.textColor = self.theme.primaryColor + cell.backgroundColor = self.theme.backgroundColor + cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if tableView === self.tableView { + self.itemSelected(self.sections[indexPath.section].1[indexPath.row]) + } else { + self.itemSelected(self.searchResults[indexPath.row]) + } + } } diff --git a/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/TelegramUI/AuthorizationSequencePasswordEntryController.swift index b6a187ce0b..8c457e9eb7 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -92,6 +92,12 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } } + func passwordIsInvalid() { + if self.isNodeLoaded { + self.controllerNode.passwordIsInvalid() + } + } + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift index 183bb4d883..d9b6e22f01 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift @@ -26,6 +26,8 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT var didForgotWithNoRecovery = false + private var clearOnce: Bool = false + var inProgress: Bool = false { didSet { self.codeField.alpha = self.inProgress ? 0.6 : 1.0 @@ -142,8 +144,19 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.codeField.layer.addShakeAnimation() } - @objc func passwordFieldTextChanged(_ textField: UITextField) { - + func passwordIsInvalid() { + self.clearOnce = true + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if self.clearOnce { + self.clearOnce = false + if range.length > string.count { + textField.text = "" + return false + } + } + return true } func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index 647e3a0071..bcb9cf0965 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -1,14 +1,18 @@ import Foundation import Display import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore final class AuthorizationSequencePhoneEntryController: ViewController { private var controllerNode: AuthorizationSequencePhoneEntryControllerNode { return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode } + private let network: Network private let strings: PresentationStrings private let theme: AuthorizationTheme + private let openUrl: (String) -> Void private var currentData: (Int32, String)? @@ -25,11 +29,15 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } var loginWithNumber: ((String) -> Void)? + private let termsDisposable = MetaDisposable() + private let hapticFeedback = HapticFeedback() - init(strings: PresentationStrings, theme: AuthorizationTheme) { + init(network: Network, strings: PresentationStrings, theme: AuthorizationTheme, openUrl: @escaping (String) -> Void) { + self.network = network self.strings = strings self.theme = theme + self.openUrl = openUrl super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) @@ -44,6 +52,10 @@ final class AuthorizationSequencePhoneEntryController: ViewController { fatalError("init(coder:) has not been implemented") } + deinit { + self.termsDisposable.dispose() + } + func updateData(countryCode: Int32, number: String) { if self.currentData == nil || self.currentData! != (countryCode, number) { self.currentData = (countryCode, number) @@ -68,10 +80,16 @@ final class AuthorizationSequencePhoneEntryController: ViewController { strongSelf.controllerNode.activateInput() } } + controller.dismissed = { + self?.controllerNode.activateInput() + } strongSelf.controllerNode.view.endEditing(true) strongSelf.present(controller, in: .window(.root)) } } + self.controllerNode.checkPhone = { [weak self] in + self?.nextPressed() + } } override func viewWillAppear(_ animated: Bool) { @@ -93,7 +111,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } @objc func nextPressed() { - let (_, number) = self.controllerNode.codeAndNumber + let (code, number) = self.controllerNode.codeAndNumber if !number.isEmpty { self.loginWithNumber?(self.controllerNode.currentNumber) } else { diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index 65515d4d8a..e6a3712819 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -22,13 +22,17 @@ private func emojiFlagForISOCountryCode(_ countryCode: NSString) -> String { } private final class PhoneAndCountryNode: ASDisplayNode { + let strings: PresentationStrings let countryButton: ASButtonNode let phoneBackground: ASImageNode let phoneInputNode: PhoneInputNode var selectCountryCode: (() -> Void)? + var checkPhone: (() -> Void)? init(strings: PresentationStrings, theme: AuthorizationTheme) { + self.strings = strings + let countryButtonBackground = generateImage(CGSize(width: 61.0, height: 67.0), rotatedContext: { size, context in let arrowSize: CGFloat = 10.0 let lineWidth = UIScreenPixel @@ -115,7 +119,8 @@ private final class PhoneAndCountryNode: ASDisplayNode { if let strongSelf = self { if let code = Int(code), let (countryId, countryName) = countryCodeToIdAndName[code] { let flagString = emojiFlagForISOCountryCode(countryId as NSString) - strongSelf.countryButton.setTitle("\(flagString) \(countryName)", with: Font.regular(20.0), with: theme.primaryColor, for: []) + let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(countryId, strings: strongSelf.strings) ?? countryName + strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(20.0), with: theme.primaryColor, for: []) } else { strongSelf.countryButton.setTitle(strings.Login_SelectCountry_Title, with: Font.regular(20.0), with: theme.textPlaceholderColor, for: []) } @@ -123,6 +128,9 @@ private final class PhoneAndCountryNode: ASDisplayNode { } self.phoneInputNode.number = "+1" + self.phoneInputNode.returnAction = { [weak self] in + self?.checkPhone?() + } } @objc func countryPressed() { @@ -155,7 +163,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { private let titleNode: ASTextNode private let noticeNode: ASTextNode private let phoneAndCountryNode: PhoneAndCountryNode - private let termsOfServiceNode: ASTextNode + private let termsOfServiceNode: ImmediateTextNode var currentNumber: String { return self.phoneAndCountryNode.phoneInputNode.number @@ -170,6 +178,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } var selectCountryCode: (() -> Void)? + var checkPhone: (() -> Void)? var inProgress: Bool = false { didSet { @@ -193,12 +202,13 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.noticeNode.displaysAsynchronously = false self.noticeNode.attributedText = NSAttributedString(string: strings.Login_PhoneAndCountryHelp, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.termsOfServiceNode = ASTextNode() - self.termsOfServiceNode.isLayerBacked = true + self.termsOfServiceNode = ImmediateTextNode() + self.termsOfServiceNode.maximumNumberOfLines = 0 + self.termsOfServiceNode.textAlignment = .center self.termsOfServiceNode.displaysAsynchronously = false let termsOfServiceAttributes = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.primaryColor) - let termsOfServiceLinkAttributes = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.accentColor) + let termsOfServiceLinkAttributes = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.accentColor, additionalAttributes: [NSAttributedStringKey.underlineStyle.rawValue: NSUnderlineStyle.styleSingle.rawValue as NSNumber, TelegramTextAttributes.Url: ""]) let termsString = parseMarkdownIntoAttributedString(self.strings.Login_TermsOfServiceLabel.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: termsOfServiceAttributes, bold: termsOfServiceAttributes, link: termsOfServiceLinkAttributes, linkAttribute: { _ in return nil @@ -216,13 +226,29 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.backgroundColor = theme.backgroundColor self.addSubnode(self.titleNode) - self.addSubnode(self.termsOfServiceNode) + //self.addSubnode(self.termsOfServiceNode) self.addSubnode(self.noticeNode) self.addSubnode(self.phoneAndCountryNode) self.phoneAndCountryNode.selectCountryCode = { [weak self] in self?.selectCountryCode?() } + self.phoneAndCountryNode.checkPhone = { [weak self] in + self?.checkPhone?() + } + + self.termsOfServiceNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.Url) + } else { + return nil + } + } + self.termsOfServiceNode.tapAttributeAction = { [weak self] attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { + } + } + self.termsOfServiceNode.linkHighlightColor = theme.accentColor.withAlphaComponent(0.5) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -237,13 +263,13 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) let noticeSize = self.noticeNode.measure(CGSize(width: min(274.0, layout.size.width - 28.0), height: CGFloat.greatestFiniteMagnitude)) - let termsOfServiceSize = self.termsOfServiceNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) + let termsOfServiceSize = self.termsOfServiceNode.updateLayout(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) var items: [AuthorizationLayoutItem] = [ AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 54.0, maxValue: 54.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), - AuthorizationLayoutItem(node: self.termsOfServiceNode, size: termsOfServiceSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 90.0, maxValue: 90.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + //AuthorizationLayoutItem(node: self.termsOfServiceNode, size: termsOfServiceSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 90.0, maxValue: 90.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), ] if layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 10.0)), items: items, transition: transition, failIfDoesNotFit: true) { diff --git a/TelegramUI/AuthorizationSequenceSignUpController.swift b/TelegramUI/AuthorizationSequenceSignUpController.swift index 764eed5c27..537f5bbf9d 100644 --- a/TelegramUI/AuthorizationSequenceSignUpController.swift +++ b/TelegramUI/AuthorizationSequenceSignUpController.swift @@ -33,7 +33,9 @@ final class AuthorizationSequenceSignUpController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) + self.statusBar.statusBarStyle = self.theme.statusBarStyle + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -41,7 +43,7 @@ final class AuthorizationSequenceSignUpController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequenceSignUpControllerNode() + self.displayNode = AuthorizationSequenceSignUpControllerNode(theme: self.theme, strings: self.strings) self.displayNodeDidLoad() self.controllerNode.signUpWithName = { [weak self] _, _ in diff --git a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift index 74a92c4f2a..56eb37063c 100644 --- a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift @@ -3,8 +3,9 @@ import AsyncDisplayKit import Display final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFieldDelegate { - private let navigationBackgroundNode: ASDisplayNode - private let stripeNode: ASDisplayNode + private let theme: AuthorizationTheme + private let strings: PresentationStrings + private let titleNode: ASTextNode private let currentOptionNode: ASTextNode @@ -30,48 +31,45 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel } } - override init() { - self.navigationBackgroundNode = ASDisplayNode() - self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) - - self.stripeNode = ASDisplayNode() - self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + init(theme: AuthorizationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: "Your Info", font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.light(30.0), textColor: theme.primaryColor) self.currentOptionNode = ASTextNode() self.currentOptionNode.isLayerBacked = true self.currentOptionNode.displaysAsynchronously = false - self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center) + self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.firstSeparatorNode = ASDisplayNode() self.firstSeparatorNode.isLayerBacked = true - self.firstSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + self.firstSeparatorNode.backgroundColor = self.theme.separatorColor self.lastSeparatorNode = ASDisplayNode() self.lastSeparatorNode.isLayerBacked = true - self.lastSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) + self.lastSeparatorNode.backgroundColor = self.theme.separatorColor self.firstNameField = TextFieldNode() self.firstNameField.textField.font = Font.regular(20.0) + self.firstNameField.textField.textColor = self.theme.primaryColor self.firstNameField.textField.textAlignment = .natural self.firstNameField.textField.returnKeyType = .next - self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3)) + self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: self.strings.UserInfo_FirstNamePlaceholder, font: self.firstNameField.textField.font, textColor: self.theme.textPlaceholderColor) self.lastNameField = TextFieldNode() self.lastNameField.textField.font = Font.regular(20.0) + self.lastNameField.textField.textColor = self.theme.primaryColor self.lastNameField.textField.textAlignment = .natural self.lastNameField.textField.returnKeyType = .done - self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3)) + self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: strings.UserInfo_LastNamePlaceholder, font: self.lastNameField.textField.font, textColor: self.theme.textPlaceholderColor) self.addPhotoButton = HighlightableButtonNode() - self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(rgb: 0xbcbcc3), paragraphAlignment: .center), for: .normal) - self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(rgb: 0xbcbcc3)), for: .normal) + self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "\(self.strings.Login_InfoAvatarAdd)\n\(self.strings.Login_InfoAvatarPhoto)", font: Font.regular(16.0), textColor: self.theme.textPlaceholderColor, paragraphAlignment: .center), for: .normal) + self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: self.theme.textPlaceholderColor), for: .normal) super.init() @@ -79,13 +77,11 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel return UITracingLayerView() }) - self.backgroundColor = UIColor.white + self.backgroundColor = self.theme.backgroundColor self.firstNameField.textField.delegate = self self.lastNameField.textField.delegate = self - self.addSubnode(self.navigationBackgroundNode) - self.addSubnode(self.stripeNode) self.addSubnode(self.firstSeparatorNode) self.addSubnode(self.lastSeparatorNode) self.addSubnode(self.firstNameField) @@ -98,8 +94,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel } func updateData(firstName: String, lastName: String) { - self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) - self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) + self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: self.theme.textPlaceholderColor) + self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: self.theme.textPlaceholderColor) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -109,9 +105,9 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel let availableHeight = max(1.0, layout.size.height - insets.top - insets.bottom) if max(layout.size.width, layout.size.height) > 1023.0 { - self.titleNode.attributedText = NSAttributedString(string: "Your Info", font: Font.light(40.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.light(40.0), textColor: self.theme.primaryColor) } else { - self.titleNode.attributedText = NSAttributedString(string: "Your Info", font: Font.light(30.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.light(30.0), textColor: self.theme.primaryColor) } let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude)) @@ -146,9 +142,6 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel navigationHeight = floor(availableHeight * 0.3) } - transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) - transition.updateFrame(node: self.stripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - let titleOffset: CGFloat if navigationHeight * 0.5 < titleSize.height + minimalTitleSpacing { titleOffset = floor((navigationHeight - titleSize.height) / 2.0) diff --git a/TelegramUI/AuthorizationTheme.swift b/TelegramUI/AuthorizationTheme.swift index ba7d570a0f..a49ba9025b 100644 --- a/TelegramUI/AuthorizationTheme.swift +++ b/TelegramUI/AuthorizationTheme.swift @@ -20,8 +20,9 @@ public final class AuthorizationTheme { let destructiveColor: UIColor let disclosureControlColor: UIColor let textPlaceholderColor: UIColor + let alertBackgroundColor: UIColor - init(statusBarStyle: StatusBarStyle, navigationBarBackgroundColor: UIColor, navigationBarTextColor: UIColor, navigationBarSeparatorColor: UIColor, searchBarBackgroundColor: UIColor, searchBarFillColor: UIColor, searchBarPlaceholderColor: UIColor, searchBarTextColor: UIColor, keyboardAppearance: UIKeyboardAppearance, backgroundColor: UIColor, primaryColor: UIColor, separatorColor: UIColor, itemHighlightedBackgroundColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disclosureControlColor: UIColor, textPlaceholderColor: UIColor) { + init(statusBarStyle: StatusBarStyle, navigationBarBackgroundColor: UIColor, navigationBarTextColor: UIColor, navigationBarSeparatorColor: UIColor, searchBarBackgroundColor: UIColor, searchBarFillColor: UIColor, searchBarPlaceholderColor: UIColor, searchBarTextColor: UIColor, keyboardAppearance: UIKeyboardAppearance, backgroundColor: UIColor, primaryColor: UIColor, separatorColor: UIColor, itemHighlightedBackgroundColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disclosureControlColor: UIColor, textPlaceholderColor: UIColor, alertBackgroundColor: UIColor) { self.statusBarStyle = statusBarStyle self.navigationBarBackgroundColor = navigationBarBackgroundColor self.navigationBarTextColor = navigationBarTextColor @@ -39,6 +40,7 @@ public final class AuthorizationTheme { self.destructiveColor = destructiveColor self.disclosureControlColor = disclosureControlColor self.textPlaceholderColor = textPlaceholderColor + self.alertBackgroundColor = alertBackgroundColor } } @@ -59,7 +61,8 @@ let defaultLightAuthorizationTheme = AuthorizationTheme( accentColor: .blue, destructiveColor: .red, disclosureControlColor: .lightGray, - textPlaceholderColor: .lightGray + textPlaceholderColor: .lightGray, + alertBackgroundColor: .white ) let defaultAuthorizationTheme = AuthorizationTheme( @@ -79,6 +82,7 @@ let defaultAuthorizationTheme = AuthorizationTheme( accentColor: .white, destructiveColor: UIColor(rgb: 0xFF736B), disclosureControlColor: UIColor(rgb: 0x717171), - textPlaceholderColor: UIColor(rgb: 0x4d4d4d) + textPlaceholderColor: UIColor(rgb: 0x4d4d4d), + alertBackgroundColor: UIColor(rgb: 0x1c1c1c) ) diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index 8b5a58facd..a34eb86d19 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -124,7 +124,7 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.masterEnabled ? 1 : 0, forKey: "autoplayGifs") + encoder.encodeInt32(self.masterEnabled ? 1 : 0, forKey: "masterEnabled") encoder.encodeObject(self.peers, forKey: "peers") encoder.encodeInt32(self.autoplayGifs ? 1 : 0, forKey: "autoplayGifs") encoder.encodeInt32(self.saveIncomingPhotos ? 1 : 0, forKey: "siph") @@ -139,7 +139,7 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { } } -public func currentAutomaticMediaDownloadSettings(postbox: Postbox) -> Signal { +public func updatedAutomaticMediaDownloadSettings(postbox: Postbox) -> Signal { return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings]) |> map { view -> AutomaticMediaDownloadSettings in let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings @@ -153,8 +153,8 @@ public func currentAutomaticMediaDownloadSettings(postbox: Postbox) -> Signal AutomaticMediaDownloadSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { entry in let currentSettings: AutomaticMediaDownloadSettings if let entry = entry as? AutomaticMediaDownloadSettings { currentSettings = entry diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 9735151706..4827270585 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -97,6 +97,7 @@ class AvatarGalleryController: ViewController { } private let account: Account + private let peer: Peer private var presentationData: PresentationData @@ -128,6 +129,7 @@ class AvatarGalleryController: ViewController { init(account: Account, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { self.account = account + self.peer = peer self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController @@ -164,7 +166,9 @@ class AvatarGalleryController: ViewController { strongSelf.entries = entries strongSelf.centralEntryIndex = 0 if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: $0) }), centralItemIndex: 0, keepFirst: true) + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: entry, delete: strongSelf.peer.id == strongSelf.account.peerId ? { + self?.deleteEntry(entry) + } : nil) }), centralItemIndex: 0, keepFirst: true) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in strongSelf?.didSetReady = true @@ -298,7 +302,9 @@ class AvatarGalleryController: ViewController { } let presentationData = self.presentationData - self.galleryNode.pager.replaceItems(self.entries.map({ PeerAvatarImageGalleryItem(account: self.account, strings: presentationData.strings, entry: $0) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(account: self.account, strings: presentationData.strings, entry: entry, delete: self.peer.id == self.account.peerId ? { [weak self] in + self?.deleteEntry(entry) + } : nil) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { @@ -376,4 +382,23 @@ class AvatarGalleryController: ViewController { } } } + + private func deleteEntry(_ entry: AvatarGalleryEntry) { + switch entry { + case .topImage: + break + case let .image(image, _): + if let reference = image.reference { + let _ = removeAccountPhoto(network: self.account.network, reference: reference).start() + } + if entry == self.entries.first { + self.dismiss(forceAway: true) + } else { + if let index = self.entries.index(of: entry) { + self.entries.remove(at: index) + self.galleryNode.pager.transaction(GalleryPagerTransaction(deleteItems: [index], insertItems: [], updateItems: [], focusOnItem: index - 1)) + } + } + } + } } diff --git a/TelegramUI/AvatarGalleryItemFooterContentNode.swift b/TelegramUI/AvatarGalleryItemFooterContentNode.swift new file mode 100644 index 0000000000..48f7d31c37 --- /dev/null +++ b/TelegramUI/AvatarGalleryItemFooterContentNode.swift @@ -0,0 +1,93 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import Photos + +private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: .white) +private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionAction"), color: .white) + +final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { + private let account: Account + + private let deleteButton: UIButton + private let actionButton: UIButton + + var delete: (() -> Void)? { + didSet { + self.deleteButton.isHidden = self.delete == nil + } + } + + var share: ((GalleryControllerInteraction) -> Void)? + + init(account: Account) { + self.account = account + + self.deleteButton = UIButton() + self.deleteButton.isHidden = true + self.actionButton = UIButton() + + self.deleteButton.setImage(deleteImage, for: [.normal]) + self.actionButton.setImage(actionImage, for: [.normal]) + + super.init() + + self.view.addSubview(self.deleteButton) + self.view.addSubview(self.actionButton) + + self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) + self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside]) + } + + deinit { + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var panelHeight: CGFloat = 44.0 + bottomInset + panelHeight += contentInset + + self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) + self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) + + return panelHeight + } + + override func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.deleteButton.alpha = 1.0 + self.actionButton.alpha = 1.0 + } + + override func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.deleteButton.alpha = 0.0 + self.actionButton.alpha = 0.0 + } + + @objc private func deleteButtonPressed() { + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.delete?() + }) + ] + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.controllerInteraction?.presentController(actionSheet, nil) + } + + @objc private func actionButtonPressed() { + if let controllerInteraction = self.controllerInteraction { + self.share?(controllerInteraction) + } + } +} diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index a0c9c08251..3bbcccad98 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -8,48 +8,37 @@ private final class BlockedPeersControllerArguments { let account: Account let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let addPeer: () -> Void let removePeer: (PeerId) -> Void let openPeer: (Peer) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { + init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { self.account = account self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.addPeer = addPeer self.removePeer = removePeer self.openPeer = openPeer } } private enum BlockedPeersSection: Int32 { + case actions case peers } private enum BlockedPeersEntryStableId: Hashable { + case add case peer(PeerId) - - var hashValue: Int { - switch self { - case let .peer(peerId): - return peerId.hashValue - } - } - - static func ==(lhs: BlockedPeersEntryStableId, rhs: BlockedPeersEntryStableId) -> Bool { - switch lhs { - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - } - } } private enum BlockedPeersEntry: ItemListNodeEntry { + case add(PresentationTheme, String) case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { + case .add: + return BlockedPeersSection.actions.rawValue case .peerItem: return BlockedPeersSection.peers.rawValue } @@ -57,6 +46,8 @@ private enum BlockedPeersEntry: ItemListNodeEntry { var stableId: BlockedPeersEntryStableId { switch self { + case .add: + return .add case let .peerItem(_, _, _, peer, _, _): return .peer(peer.id) } @@ -64,37 +55,51 @@ private enum BlockedPeersEntry: ItemListNodeEntry { static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { + case let .add(lhsTheme, lhsText): + if case let .add(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { return false } - if lhsTheme !== rhsTheme { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if !lhsPeer.isEqual(rhsPeer) { + return false + } + if lhsEditing != rhsEditing { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { return false } - if lhsStrings !== rhsStrings { - return false - } - if !lhsPeer.isEqual(rhsPeer) { - return false - } - if lhsEditing != rhsEditing { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - return true - } else { - return false - } } } static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool { switch lhs { + case .add: + if case .add = rhs { + return true + } else { + return false + } case let .peerItem(index, _, _, _, _, _): switch rhs { + case .add: + return false case let .peerItem(rhsIndex, _, _, _, _, _): return index < rhsIndex } @@ -103,6 +108,10 @@ private enum BlockedPeersEntry: ItemListNodeEntry { func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem { switch self { + case let .add(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.addPeer() + }) case let .peerItem(_, theme, strings, peer, editing, enabled): return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { arguments.openPeer(peer) @@ -163,6 +172,8 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s var entries: [BlockedPeersEntry] = [] if let peers = peers { + entries.append(.add(presentationData.theme, presentationData.strings.Conversation_BlockUser)) + var index: Int32 = 0 for peer in peers { entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id)) @@ -181,6 +192,7 @@ public func blockedPeersController(account: Account) -> ViewController { } var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() @@ -197,6 +209,46 @@ public func blockedPeersController(account: Account) -> ViewController { return state } } + }, addPeer: { + let controller = PeerSelectionController(account: account, filter: [.onlyUsers]) + controller.peerSelected = { [weak controller] peerId in + if let strongController = controller { + strongController.inProgress = true + let applyPeers: Signal = peersPromise.get() + |> filter { $0 != nil } + |> take(1) + |> mapToSignal { peers -> Signal<([Peer]?, Peer?), NoError> in + return account.postbox.transaction { transaction -> ([Peer]?, Peer?) in + return (peers, transaction.getPeer(peerId)) + } + } + |> deliverOnMainQueue + |> mapToSignal { peers, peer -> Signal in + if let peers = peers, let peer = peer { + var updatedPeers = peers + for i in 0 ..< updatedPeers.count { + if updatedPeers[i].id == peerId { + updatedPeers.remove(at: i) + break + } + } + updatedPeers.insert(peer, at: 0) + peersPromise.set(.single(updatedPeers)) + } + + return .complete() + } + removePeerDisposable.set((requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: true) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + if let strongController = controller { + strongController.inProgress = false + strongController.dismiss() + } + })) + } + } + presentControllerImpl?(controller, nil) }, removePeer: { memberId in updateState { return $0.withUpdatedRemovingPeerId(memberId) @@ -229,7 +281,6 @@ public func blockedPeersController(account: Account) -> ViewController { updateState { return $0.withUpdatedRemovingPeerId(nil) } - })) }, openPeer: { peer in if let controller = peerInfoController(account: account, peer: peer) { @@ -289,5 +340,10 @@ public func blockedPeersController(account: Account) -> ViewController { (controller.navigationController as? NavigationController)?.pushViewController(c) } } + presentControllerImpl = { [weak controller] c, a in + if let controller = controller { + controller.present(c, in: .window(.root), with: a) + } + } return controller } diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index 64025b5e54..389aacc8dc 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -609,8 +609,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, 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) + let _ = (self.account.postbox.transaction({ transaction -> Peer? in + return transaction.getPeer(botPeerId) }) |> deliverOnMainQueue).start(next: { [weak self] botPeer in if let strongSelf = self, let botPeer = botPeer { let request = PKPaymentRequest() @@ -664,8 +664,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if !liabilityNoticeAccepted { let messageId = self.messageId - let botPeer: Signal = self.account.postbox.modify { modifier -> Peer? in - if let message = modifier.getMessage(messageId) { + let botPeer: Signal = self.account.postbox.transaction { transaction -> Peer? in + if let message = transaction.getMessage(messageId) { return message.author } return nil diff --git a/TelegramUI/CachedChannelAdmins.swift b/TelegramUI/CachedChannelAdmins.swift new file mode 100644 index 0000000000..3afbd153e5 --- /dev/null +++ b/TelegramUI/CachedChannelAdmins.swift @@ -0,0 +1,38 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +final class CachedChannelAdminIds: PostboxCoding { + let ids: Set + + init(ids: Set) { + self.ids = ids + } + + init(decoder: PostboxDecoder) { + self.ids = Set(decoder.decodeInt64ArrayForKey("ids").map(PeerId.init)) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64Array(Array(self.ids.map({ $0.toInt64() })), forKey: "ids") + } + + static func cacheKey(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200) + +func cachedChannelAdminIdsEntryId(peerId: PeerId) -> ItemCacheEntryId { + return ItemCacheEntryId(collectionId: 100, key: CachedChannelAdminIds.cacheKey(peerId: peerId)) +} + +func updateCachedChannelAdminIds(postbox: Postbox, peerId: PeerId, ids: Set) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: 100, key: CachedChannelAdminIds.cacheKey(peerId: peerId)), entry: CachedChannelAdminIds(ids: ids), collectionSpec: collectionSpec) + } +} diff --git a/TelegramUI/CallKitIntergation.swift b/TelegramUI/CallKitIntergation.swift index fc563d3686..ca55dec8b6 100644 --- a/TelegramUI/CallKitIntergation.swift +++ b/TelegramUI/CallKitIntergation.swift @@ -13,14 +13,19 @@ final class CallKitIntegration { } init?(startCall: @escaping (UUID, String) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, audioSessionActivationChanged: @escaping (Bool) -> Void) { - #if (arch(i386) || arch(x86_64)) && os(iOS) + if Locale.current.regionCode?.lowercased() == "cn" { return nil + } + + #if (arch(i386) || arch(x86_64)) && os(iOS) + return nil #else - if #available(iOSApplicationExtension 10.0, *) { - self.providerDelegate = CallKitProviderDelegate(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, audioSessionActivationChanged: audioSessionActivationChanged) - } else { - return nil - } + + if #available(iOSApplicationExtension 10.0, *) { + self.providerDelegate = CallKitProviderDelegate(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, audioSessionActivationChanged: audioSessionActivationChanged) + } else { + return nil + } #endif } diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift index 01b731e965..2b1239b3b9 100644 --- a/TelegramUI/CallListCallItem.swift +++ b/TelegramUI/CallListCallItem.swift @@ -551,7 +551,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) } }) } else { diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index 52503b6d53..cd6f42ee78 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -247,8 +247,8 @@ public final class CallListController: ViewController { self.account.telegramApplicationContext.navigateToCurrentCall?() } else { let presentationData = self.presentationData - let _ = (self.account.postbox.modify { modifier -> (Peer?, Peer?) in - return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + let _ = (self.account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { [weak self] peer, current in if let strongSelf = self, let peer = peer, let current = current { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), 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: { diff --git a/TelegramUI/CallListSettings.swift b/TelegramUI/CallListSettings.swift index e59bba2368..2e156d3e5e 100644 --- a/TelegramUI/CallListSettings.swift +++ b/TelegramUI/CallListSettings.swift @@ -39,8 +39,8 @@ public struct CallListSettings: PreferencesEntry, Equatable { } func updateCallListSettingsInteractively(postbox: Postbox, _ f: @escaping (CallListSettings) -> CallListSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in let currentSettings: CallListSettings if let entry = entry as? CallListSettings { currentSettings = entry diff --git a/TelegramUI/ChangePhoneNumberIntroController.swift b/TelegramUI/ChangePhoneNumberIntroController.swift index abd16ab139..1c14148ac8 100644 --- a/TelegramUI/ChangePhoneNumberIntroController.swift +++ b/TelegramUI/ChangePhoneNumberIntroController.swift @@ -26,7 +26,7 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { return UITracingLayerView() }) - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeTextColor) + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeMonoIcon) let textColor = self.presentationData.theme.list.freeTextColor self.labelNode.attributedText = parseMarkdownIntoAttributedString(self.presentationData.strings.PhoneNumberHelp_Help, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: textColor), link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), linkAttribute: { _ in return nil }), textAlignment: .center) self.buttonNode.setTitle(self.presentationData.strings.PhoneNumberHelp_ChangeNumber, with: Font.regular(19.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index e963d80272..ff877c33c3 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -466,7 +466,7 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe updateState { current in return current.withUpdatedUpdating(true) } - updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in + updateRightsDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: account, peerId: peerId, memberId: adminId, adminRights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(TelegramChannelAdminRights(flags: [])) @@ -499,7 +499,7 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else if canEdit { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - if let _ = initialParticipant { + if let initialParticipant = initialParticipant, let channel = channelView.peers[channelView.peerId] as? TelegramChannel { var updateFlags: TelegramChannelAdminRightsFlags? updateState { current in updateFlags = current.updatedFlags @@ -510,13 +510,43 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe } } + if updateFlags == nil { + switch initialParticipant { + case .creator: + break + case let .member(member): + if member.adminInfo?.rights == nil { + let maskRightsFlags: TelegramChannelAdminRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } + + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting(.canAddAdmins) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.flags).subtracting(.canAddAdmins) + } else { + updateFlags = [] + } + } + } + } + if let updateFlags = updateFlags { - updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: account, peerId: peerId, memberId: adminId, adminRights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(TelegramChannelAdminRights(flags: updateFlags)) dismissImpl?() })) + } else { + dismissImpl?() } } else if canEdit, let channel = channelView.peers[channelView.peerId] as? TelegramChannel { var updateFlags: TelegramChannelAdminRightsFlags? @@ -544,7 +574,10 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe } if let updateFlags = updateFlags { - updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: account, peerId: peerId, memberId: adminId, adminRights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(TelegramChannelAdminRights(flags: updateFlags)) diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index b6fe654896..392c79b54e 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -537,135 +537,49 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon updateState { return $0.withUpdatedRemovingPeerId(adminId) } - let applyPeers: Signal = adminsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - if let peers = peers { - var updatedPeers = peers - for i in 0 ..< updatedPeers.count { - if updatedPeers[i].peer.id == adminId { - updatedPeers.remove(at: i) - break - } - } - adminsPromise.set(.single(updatedPeers)) - } - - return .complete() - } - - removeAdminDisposable.set((removePeerAdmin(account: account, peerId: peerId, adminId: adminId) - |> then(applyPeers |> mapError { _ -> RemovePeerAdminError in return .generic }) |> deliverOnMainQueue).start(error: { _ in + removeAdminDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: account, peerId: peerId, memberId: adminId, adminRights: TelegramChannelAdminRights(flags: [])) + |> deliverOnMainQueue).start(completed: { updateState { return $0.withUpdatedRemovingPeerId(nil) } - }, completed: { - updateState { state in - var updatedTemporaryAdmins = state.temporaryAdmins - for i in 0 ..< updatedTemporaryAdmins.count { - if updatedTemporaryAdmins[i].peer.id == adminId { - updatedTemporaryAdmins.remove(at: i) - break - } - } - return state.withUpdatedRemovingPeerId(nil).withUpdatedTemporaryAdmins(updatedTemporaryAdmins) - } })) }, addAdmin: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer in - presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: peer.id, initialParticipant: nil, updated: { updatedRights in - let applyAdmin: Signal = adminsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> mapToSignal { admins -> Signal in - return account.postbox.loadedPeerWithId(account.peerId) - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { accountPeer in - if let admins = admins { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - var updatedAdmins = admins - if updatedRights.isEmpty { - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == peer.id { - updatedAdmins.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == peer.id { - if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: updatedAdmins[i].peers) - } - found = true - break - } - } - if !found { - updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer, peers: [accountPeer.id: accountPeer])) - } - } - adminsPromise.set(.single(updatedAdmins)) - } - - return .complete() + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer, participant in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + if peer.id == account.peerId { + return + } + if let participant = participant { + switch participant.participant { + case .creator: + return + case let .member(_, _, _, banInfo): + if let banInfo = banInfo, banInfo.restrictedBy != account.peerId { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Channel_Members_AddAdminErrorBlacklisted, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + return } } - addAdminDisposable.set(applyAdmin.start()) + } + presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: peer.id, initialParticipant: participant?.participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openAdmin: { participant in if case let .member(adminId, _, _, _) = participant { - presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { updatedRights in - let applyAdmin: Signal = adminsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { admins -> Signal in - if let admins = admins { - var updatedAdmins = admins - if updatedRights.isEmpty { - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == adminId { - updatedAdmins.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == adminId { - if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: updatedAdmins[i].peers) - } - found = true - break - } - } - if !found { - //updatedAdmins.append(RenderedChannelParticipant(participant: .member(id, date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: peer)) - } - } - adminsPromise.set(.single(updatedAdmins)) - } - - return .complete() - } - addAdminDisposable.set(applyAdmin.start()) + presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue - let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) }) - - adminsPromise.set(adminsSignal) + let (membersDisposable, loadMoreControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, peerId: peerId) { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(.single(nil)) + } else { + adminsPromise.set(.single(membersState.list)) + } + } + actionsDisposable.add(membersDisposable) var previousPeers: [RenderedChannelParticipant]? @@ -714,5 +628,10 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon controller.present(c, in: .window(.root), with: p) } } + controller.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + } return controller } diff --git a/TelegramUI/ChannelBannedMemberController.swift b/TelegramUI/ChannelBannedMemberController.swift index d25594e47d..a8c36c4cd2 100644 --- a/TelegramUI/ChannelBannedMemberController.swift +++ b/TelegramUI/ChannelBannedMemberController.swift @@ -176,7 +176,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { }, avatarTapped: { }) case let .rightItem(theme, _, text, right, flags, value, enabled): - return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in + return ItemListSwitchItem(theme: theme, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in arguments.toggleRight(right, flags) }) case let .timeout(theme, text, value): @@ -188,17 +188,22 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { } private struct ChannelBannedMemberControllerState: Equatable { + let referenceTimestamp: Int32 let updatedFlags: TelegramChannelBannedRightsFlags? let updatedTimeout: Int32? let updating: Bool - init(updatedFlags: TelegramChannelBannedRightsFlags? = nil, updatedTimeout: Int32? = nil, updating: Bool = false) { + init(referenceTimestamp: Int32, updatedFlags: TelegramChannelBannedRightsFlags? = nil, updatedTimeout: Int32? = nil, updating: Bool = false) { + self.referenceTimestamp = referenceTimestamp self.updatedFlags = updatedFlags self.updatedTimeout = updatedTimeout self.updating = updating } static func ==(lhs: ChannelBannedMemberControllerState, rhs: ChannelBannedMemberControllerState) -> Bool { + if lhs.referenceTimestamp != rhs.referenceTimestamp { + return false + } if lhs.updatedFlags != rhs.updatedFlags { return false } @@ -212,15 +217,15 @@ private struct ChannelBannedMemberControllerState: Equatable { } func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelBannedRightsFlags?) -> ChannelBannedMemberControllerState { - return ChannelBannedMemberControllerState(updatedFlags: updatedFlags, updatedTimeout: self.updatedTimeout, updating: self.updating) + return ChannelBannedMemberControllerState(referenceTimestamp: self.referenceTimestamp, updatedFlags: updatedFlags, updatedTimeout: self.updatedTimeout, updating: self.updating) } - func withUpdatedUpdatedTimeout(_ updatedTimeoyt: Int32?) -> ChannelBannedMemberControllerState { - return ChannelBannedMemberControllerState(updatedFlags: self.updatedFlags, updatedTimeout: updatedTimeout, updating: self.updating) + func withUpdatedUpdatedTimeout(_ updatedTimeout: Int32?) -> ChannelBannedMemberControllerState { + return ChannelBannedMemberControllerState(referenceTimestamp: self.referenceTimestamp, updatedFlags: self.updatedFlags, updatedTimeout: updatedTimeout, updating: self.updating) } func withUpdatedUpdating(_ updating: Bool) -> ChannelBannedMemberControllerState { - return ChannelBannedMemberControllerState(updatedFlags: self.updatedFlags, updatedTimeout: self.updatedTimeout, updating: updating) + return ChannelBannedMemberControllerState(referenceTimestamp: self.referenceTimestamp, updatedFlags: self.updatedFlags, updatedTimeout: self.updatedTimeout, updating: updating) } } @@ -240,15 +245,15 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChannel } } -private func rightDependencies(_ right: TelegramChannelBannedRightsFlags) -> [TelegramChannelBannedRightsFlags] { +private func rightDependencies(_ right: TelegramChannelBannedRightsFlags) -> TelegramChannelBannedRightsFlags { if right.contains(.banReadMessages) { return [] } else if right.contains(.banSendMessages) { return [.banReadMessages] } else if right.contains(.banSendMedia) { return [.banReadMessages, .banSendMessages] - } else if right.contains(.banSendStickers) { - return [.banReadMessages, .banSendMessages] + } else if right.contains(.banSendGifs) { + return [.banReadMessages, .banSendMessages, .banSendGifs, .banSendGames, .banSendInline] } else if right.contains(.banEmbedLinks) { return [.banReadMessages, .banSendMessages] } else { @@ -256,7 +261,43 @@ private func rightDependencies(_ right: TelegramChannelBannedRightsFlags) -> [Te } } -private let initialRightFlags: TelegramChannelBannedRightsFlags = [.banSendMessages, .banSendGifs, .banSendGames, .banSendInline, .banSendStickers, .banSendMedia, .banEmbedLinks] +/* + TelegramChannelBannedRightsFlags = [ + .banReadMessages, + .banSendMessages, + .banSendMedia, + .banSendStickers, .banSendGifs, .banSendGames, .banSendInline, + .banEmbedLinks + ] + */ + +private func rightReverseDependencies(_ right: TelegramChannelBannedRightsFlags) -> TelegramChannelBannedRightsFlags { + if right.contains(.banReadMessages) { + return [.banSendMessages, .banSendMedia, .banSendStickers, .banSendGifs, .banSendGames, .banSendInline, .banEmbedLinks] + } else if right.contains(.banSendMessages) { + return [.banSendMedia, .banSendStickers, .banSendGifs, .banSendGames, .banSendInline, .banEmbedLinks] + } else if right.contains(.banSendMedia) { + return [] + } else if right.contains(.banSendGifs) { + return [.banSendStickers, .banSendGames, .banSendInline] + } else if right.contains(.banEmbedLinks) { + return [] + } else { + return [] + } +} + +private let initialRightFlags: TelegramChannelBannedRightsFlags = [.banReadMessages, .banSendMessages, .banSendGifs, .banSendGames, .banSendInline, .banSendStickers, .banSendMedia, .banEmbedLinks] + +func maskedFlags(_ flags: TelegramChannelBannedRightsFlags) -> TelegramChannelBannedRightsFlags { + return flags.intersection([ + .banReadMessages, + .banSendMessages, + .banSendMedia, + .banSendStickers, .banSendGifs, .banSendGames, .banSendInline, + .banEmbedLinks + ]) +} private func channelBannedMemberControllerEntries(presentationData: PresentationData, state: ChannelBannedMemberControllerState, accountPeerId: PeerId, channelView: PeerView, memberView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelBannedMemberEntry] { var entries: [ChannelBannedMemberEntry] = [] @@ -282,6 +323,14 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation currentTimeout = Int32.max } + let currentTimeoutString: String + if currentTimeout == 0 || currentTimeout == Int32.max { + currentTimeoutString = presentationData.strings.MessageTimer_Forever + } else { + let remainingTimeout = currentTimeout - state.referenceTimestamp + currentTimeoutString = timeIntervalString(strings: presentationData.strings, value: remainingTimeout) + } + let rightsOrder: [TelegramChannelBannedRightsFlags] = [ .banReadMessages, .banSendMessages, @@ -292,19 +341,20 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation var index = 0 for right in rightsOrder { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating)) + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right), right, currentRightsFlags, !currentRightsFlags.contains(right), !state.updating)) index += 1 } - + entries.append(.timeout(presentationData.theme, presentationData.strings.Channel_BanUser_BlockFor, currentTimeoutString)) } return entries } public func channelBannedMemberController(account: Account, peerId: PeerId, memberId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelBannedRights) -> Void) -> ViewController { - let statePromise = ValuePromise(ChannelBannedMemberControllerState(), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelBannedMemberControllerState()) + let initialState = ChannelBannedMemberControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970)) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) let updateState: ((ChannelBannedMemberControllerState) -> ChannelBannedMemberControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -315,19 +365,60 @@ public func channelBannedMemberController(account: Account, peerId: PeerId, memb actionsDisposable.add(updateRightsDisposable) var dismissImpl: (() -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let arguments = ChannelBannedMemberControllerArguments(account: account, toggleRight: { right, flags in updateState { current in var updated = flags - if flags.contains(right) { - updated.remove(right) + let compositeFlags: TelegramChannelBannedRightsFlags + switch right { + case .banSendStickers, .banSendGifs, .banSendGames, .banSendInline: + compositeFlags = [.banSendStickers, .banSendGifs, .banSendGames, .banSendInline] + default: + compositeFlags = right + + } + if !flags.intersection(compositeFlags).isEmpty { + updated = updated.subtracting(compositeFlags) + updated = updated.subtracting(rightDependencies(right)) } else { - updated.insert(right) + updated = updated.union(compositeFlags) + updated = updated.union(rightReverseDependencies(right)) } return current.withUpdatedUpdatedFlags(updated) } }, openTimeout: { - + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let intervals: [Int32] = [ + 1 * 60 * 60 * 24, + 7 * 60 * 60 * 24 + ] + let applyValue: (Int32?) -> Void = { value in + updateState { state in + let state = state.withUpdatedUpdatedTimeout(value) + return state + } + } + var items: [ActionSheetItem] = [] + for interval in intervals { + items.append(ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: interval), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + applyValue(initialState.referenceTimestamp + interval) + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Custom, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + presentControllerImpl?(PeerBanTimeoutController(theme: presentationData.theme, strings: presentationData.strings, currentValue: Int32(Date().timeIntervalSince1970), applyValue: { value in + applyValue(value) + }), nil) + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) }) let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: memberId)]) @@ -350,49 +441,84 @@ public func channelBannedMemberController(account: Account, peerId: PeerId, memb rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - if let _ = initialParticipant { + if let initialParticipant = initialParticipant { var updateFlags: TelegramChannelBannedRightsFlags? + var updateTimeout: Int32? updateState { current in updateFlags = current.updatedFlags - if let _ = updateFlags { - return current.withUpdatedUpdating(true) - } else { - return current + updateTimeout = current.updatedTimeout + return current + } + + if updateFlags == nil && updateTimeout == nil { + if case let .member(_, _, _, maybeBanInfo) = initialParticipant { + if maybeBanInfo == nil { + updateFlags = initialRightFlags + updateTimeout = Int32.max + } } } - if let updateFlags = updateFlags { - let rights = TelegramChannelBannedRights(flags: updateFlags, untilDate: Int32.max) - updateRightsDisposable.set((updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: rights) |> deliverOnMainQueue).start(error: { _ in + if updateFlags != nil || updateTimeout != nil { + updateState { current in + return current.withUpdatedUpdating(true) + } + + let currentRightsFlags: TelegramChannelBannedRightsFlags + if let updatedFlags = updateFlags { + currentRightsFlags = updatedFlags + } else if case let .member(_, _, _, maybeBanInfo) = initialParticipant, let banInfo = maybeBanInfo { + currentRightsFlags = banInfo.rights.flags + } else { + currentRightsFlags = initialRightFlags + } + + let currentTimeout: Int32 + if let updateTimeout = updateTimeout { + currentTimeout = updateTimeout + } else if case let .member(_, _, _, maybeBanInfo) = initialParticipant, let banInfo = maybeBanInfo { + currentTimeout = banInfo.rights.untilDate + } else { + currentTimeout = Int32.max + } + + let rights = TelegramChannelBannedRights(flags: maskedFlags(currentRightsFlags), untilDate: currentTimeout) + + updateRightsDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: memberId, bannedRights: rights) + |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(rights) dismissImpl?() })) + } else { + dismissImpl?() } - } else if canEdit, let channel = channelView.peers[channelView.peerId] as? TelegramChannel { + } else if canEdit, let _ = channelView.peers[channelView.peerId] as? TelegramChannel { var updateFlags: TelegramChannelBannedRightsFlags? + var updateTimeout: Int32? updateState { current in updateFlags = current.updatedFlags - if let _ = updateFlags { - return current.withUpdatedUpdating(true) - } else { - return current - } + updateTimeout = current.updatedTimeout + return current.withUpdatedUpdating(true) } if updateFlags == nil { updateFlags = initialRightFlags } + if updateTimeout == nil { + updateTimeout = Int32.max + } - if let updateFlags = updateFlags { - let rights = TelegramChannelBannedRights(flags: updateFlags, untilDate: Int32.max) - updateRightsDisposable.set((updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: rights) |> deliverOnMainQueue).start(error: { _ in - - }, completed: { - updated(rights) - dismissImpl?() - })) + if let updateFlags = updateFlags, let updateTimeout = updateTimeout { + let rights = TelegramChannelBannedRights(flags: maskedFlags(updateFlags), untilDate: updateTimeout) + updateRightsDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: memberId, bannedRights: rights) + |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(rights) + dismissImpl?() + })) } } else { dismissImpl?() @@ -413,5 +539,8 @@ public func channelBannedMemberController(account: Account, peerId: PeerId, memb dismissImpl = { [weak controller] in controller?.dismiss() } + presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.present(value, in: .window(.root), with: presentationArguments) + } return controller } diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index 31dfbfd64b..c4d6dd17ed 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -23,58 +23,53 @@ private final class ChannelBlacklistControllerArguments { private enum ChannelBlacklistSection: Int32 { case add - case peers + case restricted + case banned } private enum ChannelBlacklistEntryStableId: Hashable { - case add + case index(Int) case peer(PeerId) - - var hashValue: Int { - switch self { - case .add: - return 0 - case let .peer(peerId): - return peerId.hashValue - } - } - - static func ==(lhs: ChannelBlacklistEntryStableId, rhs: ChannelBlacklistEntryStableId) -> Bool { - switch lhs { - case .add: - if case .add = rhs { - return true - } else { - return false - } - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - } - } +} + +private enum ChannelBlacklistPeerCategory { + case restricted + case banned } private enum ChannelBlacklistEntry: ItemListNodeEntry { case add(PresentationTheme, String) - case peerItem(PresentationTheme, PresentationStrings, Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + case restrictedHeader(PresentationTheme, String) + case bannedHeader(PresentationTheme, String) + case peerItem(PresentationTheme, PresentationStrings, Int32, ChannelBlacklistPeerCategory, RenderedChannelParticipant, ItemListPeerItemEditing, Bool, Bool) var section: ItemListSectionId { switch self { case .add: return ChannelBlacklistSection.add.rawValue - case .peerItem: - return ChannelBlacklistSection.peers.rawValue + case .restrictedHeader: + return ChannelBlacklistSection.restricted.rawValue + case .bannedHeader: + return ChannelBlacklistSection.banned.rawValue + case let .peerItem(_, _, _, category, _, _, _, _): + switch category { + case .restricted: + return ChannelBlacklistSection.restricted.rawValue + case .banned: + return ChannelBlacklistSection.banned.rawValue + } } } var stableId: ChannelBlacklistEntryStableId { switch self { case .add: - return .add - case let .peerItem(_, _, _, participant, _, _): + return .index(0) + case .restrictedHeader: + return .index(1) + case .bannedHeader: + return .index(2) + case let .peerItem(_, _, _, _, participant, _, _, _): return .peer(participant.peer.id) } } @@ -87,8 +82,20 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { } else { return false } - case let .peerItem(lhsTheme, lhsStrings, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(rhsTheme, rhsStrings, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + case let .restrictedHeader(lhsTheme, lhsText): + if case let .restrictedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .bannedHeader(lhsTheme, lhsText): + if case let .bannedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peerItem(lhsTheme, lhsStrings, lhsIndex, lhsCategory, lhsParticipant, lhsEditing, lhsEnabled, lhsCanOpen): + if case let .peerItem(rhsTheme, rhsStrings, rhsIndex, rhsCategory, rhsParticipant, rhsEditing, rhsEnabled, rhsCanOpen) = rhs { if lhsTheme !== rhsTheme { return false } @@ -98,6 +105,9 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { if lhsIndex != rhsIndex { return false } + if lhsCategory != rhsCategory { + return false + } if lhsParticipant != rhsParticipant { return false } @@ -107,6 +117,9 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { if lhsEnabled != rhsEnabled { return false } + if lhsCanOpen != rhsCanOpen { + return false + } return true } else { return false @@ -123,11 +136,37 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { default: return true } - case let .peerItem(_, _, index, _, _, _): + case .restrictedHeader: switch rhs { - case .add: + case .add, .restrictedHeader: return false - case let .peerItem(_, _, rhsIndex, _, _, _): + default: + return true + } + case .bannedHeader: + switch rhs { + case .add, .restrictedHeader, .bannedHeader: + return false + case let .peerItem(_, _, _, category, _, _, _, _): + switch category { + case .restricted: + return false + case .banned: + return true + } + } + case let .peerItem(_, _, index, category, _, _, _, _): + switch rhs { + case .add, .restrictedHeader: + return false + case .bannedHeader: + switch category { + case .restricted: + return true + case .banned: + return false + } + case let .peerItem(_, _, rhsIndex, _, _, _, _, _): return index < rhsIndex } } @@ -139,7 +178,11 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { arguments.addPeer() }) - case let .peerItem(theme, strings, _, participant, editing, enabled): + case let .restrictedHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .bannedHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .peerItem(theme, strings, _, _, participant, editing, enabled, canOpen): var text: ItemListPeerItemText = .none switch participant.participant { case let .member(_, _, _, banInfo): @@ -149,9 +192,9 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { default: break } - return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: canOpen ? { arguments.openPeer(participant.participant) - }, setPeerIdWithRevealedOptions: { previousId, id in + } : nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -207,32 +250,36 @@ private struct ChannelBlacklistControllerState: Equatable { private func channelBlacklistControllerEntries(presentationData: PresentationData, view: PeerView, state: ChannelBlacklistControllerState, blacklist: ChannelBlacklist?) -> [ChannelBlacklistEntry] { var entries: [ChannelBlacklistEntry] = [] - if let blacklist = blacklist { + if let channel = view.peers[view.peerId] as? TelegramChannel, let blacklist = blacklist { entries.append(.add(presentationData.theme, presentationData.strings.Channel_Members_AddMembers)) var index: Int32 = 0 - for participant in blacklist.restricted.sorted(by: { lhs, rhs in - let lhsInvitedAt: Int32 - switch lhs.participant { - case .creator: - lhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _): - lhsInvitedAt = invitedAt - } - let rhsInvitedAt: Int32 - switch rhs.participant { - case .creator: - rhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _): - rhsInvitedAt = invitedAt - } - return lhsInvitedAt < rhsInvitedAt - }) { + if !blacklist.restricted.isEmpty { + entries.append(.restrictedHeader(presentationData.theme, presentationData.strings.Channel_BanList_RestrictedTitle)) + } + let canOpen: Bool + if case .group = channel.info { + canOpen = true + } else { + canOpen = false + } + for participant in blacklist.restricted { var editable = true if case .creator = participant.participant { editable = false } - entries.append(.peerItem(presentationData.theme, presentationData.strings, index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) + entries.append(.peerItem(presentationData.theme, presentationData.strings, index, .restricted, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, canOpen)) + index += 1 + } + if !blacklist.banned.isEmpty { + entries.append(.bannedHeader(presentationData.theme, presentationData.strings.Channel_BanList_BlockedTitle)) + } + for participant in blacklist.banned { + var editable = true + if case .creator = participant.participant { + editable = false + } + entries.append(.peerItem(presentationData.theme, presentationData.strings, index, .banned, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, canOpen)) index += 1 } } @@ -259,6 +306,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View let blacklistPromise = Promise(nil) + let arguments = ChannelBlacklistControllerArguments(account: account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { @@ -268,49 +316,20 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View } } }, addPeer: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer in - presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: peer.id, initialParticipant: nil, updated: { updatedRights in - let applyBanned: Signal = combineLatest(blacklistPromise.get() - |> filter { $0 != nil } - |> take(1), account.postbox.loadedPeerWithId(account.peerId)) - |> deliverOnMainQueue - |> mapToSignal { blacklist, accountPeer -> Signal in - if let blacklist = blacklist { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - var updatedBlacklist = blacklist.restricted - if updatedRights.flags.isEmpty { - for i in 0 ..< updatedBlacklist.count { - if updatedBlacklist[i].peer.id == peer.id { - updatedBlacklist.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedBlacklist.count { - if updatedBlacklist[i].peer.id == peer.id { - if case let .member(id, date, _, _) = updatedBlacklist[i].participant { - updatedBlacklist[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: nil, banInfo: ChannelParticipantBannedInfo(rights: updatedRights, restrictedBy: account.peerId, isMember: true)), peer: updatedBlacklist[i].peer) - } - found = true - break - } - } - if !found { - var peers: [PeerId: Peer] = [:] - peers[peer.id] = peer - peers[accountPeer.id] = accountPeer - updatedBlacklist.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: nil, banInfo: ChannelParticipantBannedInfo(rights: updatedRights, restrictedBy: account.peerId, isMember: true)), peer: peer, peers: peers)) - } - } - let updatedValue = ChannelBlacklist(banned: blacklist.banned, restricted: updatedBlacklist) - blacklistPromise.set(.single(updatedValue)) + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer, participant in + if let participant = participant { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + switch participant.participant { + case .creator: + return + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo, adminInfo.promotedBy != account.peerId { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + return } - - return .complete() } - updateBannedDisposable.set(applyBanned.start()) + } + presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: peer.id, initialParticipant: participant?.participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removePeer: { memberId in @@ -318,20 +337,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View return $0.withUpdatedRemovingPeerId(memberId) } - let applyPeers: Signal = blacklistPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { blacklist -> Signal in - if let blacklist = blacklist { - let updatedBlacklist = blacklist.withRemovedPeerId(memberId) - blacklistPromise.set(.single(updatedBlacklist)) - } - - return .complete() - } - - removePeerDisposable.set((updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: TelegramChannelBannedRights(flags: [], untilDate: 0)) |> deliverOnMainQueue).start(error: { _ in + removePeerDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: memberId, bannedRights: TelegramChannelBannedRights(flags: [], untilDate: 0)) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingPeerId(nil) } @@ -339,53 +345,41 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View updateState { return $0.withUpdatedRemovingPeerId(nil) } - let _ = applyPeers.start() })) }, openPeer: { participant in - presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { updatedRights in - let applyBanned: Signal = blacklistPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { blacklist -> Signal in - if let blacklist = blacklist { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - var updatedBlacklist = blacklist.restricted - if updatedRights.flags.isEmpty { - for i in 0 ..< updatedBlacklist.count { - if updatedBlacklist[i].peer.id == participant.peerId { - updatedBlacklist.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedBlacklist.count { - if updatedBlacklist[i].peer.id == participant.peerId { - if case let .member(id, date, _, _) = updatedBlacklist[i].participant { - updatedBlacklist[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: nil, banInfo: ChannelParticipantBannedInfo(rights: updatedRights, restrictedBy: account.peerId, isMember: true)), peer: updatedBlacklist[i].peer, peers: updatedBlacklist[i].peers) - } - found = true - break - } - } - } - let updatedValue = ChannelBlacklist(banned: blacklist.banned, restricted: updatedBlacklist) - blacklistPromise.set(.single(updatedValue)) - } - - return .complete() - } - updateBannedDisposable.set(applyBanned.start()) + presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) let peerView = account.viewTracker.peerView(peerId) - let blacklistSignal: Signal = .single(nil) |> then(channelBlacklistParticipants(account: account, peerId: peerId) |> map { Optional($0) }) - - blacklistPromise.set(blacklistSignal) + let (listDisposable, loadMoreControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.restrictedAndBanned(postbox: account.postbox, network: account.network, peerId: peerId, updated: { listState in + if case .loading = listState.loadingState, listState.list.isEmpty { + blacklistPromise.set(.single(nil)) + } else { + var restricted: [RenderedChannelParticipant] = [] + var banned: [RenderedChannelParticipant] = [] + for member in listState.list { + switch member.participant { + case let .member(_, _, _, banInfo): + if let banInfo = banInfo { + if !banInfo.rights.flags.contains(.banReadMessages) { + restricted.append(member) + } else { + banned.append(member) + } + } else { + assertionFailure() + } + default: + assertionFailure() + break + } + } + blacklistPromise.set(.single(ChannelBlacklist(banned: banned, restricted: restricted))) + } + }) + actionsDisposable.add(listDisposable) var previousBlacklist: ChannelBlacklist? @@ -431,5 +425,10 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View controller.present(c, in: .window(.root), with: p) } } + controller.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + } return controller } diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index d8ef0c9684..94d75ad33a 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -561,8 +561,8 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr })) }) }, changeProfilePhoto: { - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -711,9 +711,9 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, changeNotificationSoundSettings: { - let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in - let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings - let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + let _ = (account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings return (peerSettings, globalSettings) } |> deliverOnMainQueue).start(next: { settings in let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.privateChats.sound, completion: { sound in diff --git a/TelegramUI/ChannelMemberCategoryListContext.swift b/TelegramUI/ChannelMemberCategoryListContext.swift new file mode 100644 index 0000000000..8701e97168 --- /dev/null +++ b/TelegramUI/ChannelMemberCategoryListContext.swift @@ -0,0 +1,593 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit + +private let initialBatchSize: Int32 = 32 +private let emptyTimeout: Double = 2.0 * 60.0 +private let headUpdateTimeout: Double = 30.0 +private let requestBatchSize: Int32 = 32 + +enum ChannelMemberListLoadingState: Equatable { + case loading + case ready(hasMore: Bool) +} + +private extension ChannelParticipant { + var adminInfo: ChannelParticipantAdminInfo? { + switch self { + case .creator: + return nil + case let .member(_, _, adminInfo, _): + return adminInfo + } + } + + var banInfo: ChannelParticipantBannedInfo? { + switch self { + case .creator: + return nil + case let .member(_, _, _, banInfo): + return banInfo + } + } +} + +struct ChannelMemberListState { + let list: [RenderedChannelParticipant] + let loadingState: ChannelMemberListLoadingState + + func withUpdatedList(_ list: [RenderedChannelParticipant]) -> ChannelMemberListState { + return ChannelMemberListState(list: list, loadingState: self.loadingState) + } + + func withUpdatedLoadingState(_ loadingState: ChannelMemberListLoadingState) -> ChannelMemberListState { + return ChannelMemberListState(list: self.list, loadingState: loadingState) + } +} + +enum ChannelMemberListCategory { + case recent + case recentSearch(String) + case admins + case restricted + case banned +} + +private protocol ChannelMemberCategoryListContext { + var listStateValue: ChannelMemberListState { get } + var listState: Signal { get } + func loadMore() + func reset() + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?)]) + func forceUpdateHead() +} + +private func isParticipantMember(_ participant: ChannelParticipant) -> Bool { + if let banInfo = participant.banInfo { + return !banInfo.rights.flags.contains(.banReadMessages) && banInfo.isMember + } else { + return true + } +} + +private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategoryListContext { + private let postbox: Postbox + private let network: Network + private let peerId: PeerId + private let category: ChannelMemberListCategory + + var listStateValue: ChannelMemberListState { + didSet { + self.listStatePromise.set(.single(self.listStateValue)) + if case .admins = self.category, case .ready = self.listStateValue.loadingState { + let ids: Set = Set(self.listStateValue.list.map { $0.peer.id }) + let previousIds: Set = Set(oldValue.list.map { $0.peer.id }) + if ids != previousIds { + let _ = updateCachedChannelAdminIds(postbox: self.postbox, peerId: self.peerId, ids: ids).start() + } + } + } + } + private var listStatePromise: Promise + var listState: Signal { + return self.listStatePromise.get() + } + + private let loadingDisposable = MetaDisposable() + private let headUpdateDisposable = MetaDisposable() + + private var headUpdateTimer: SwiftSignalKit.Timer? + + init(postbox: Postbox, network: Network, peerId: PeerId, category: ChannelMemberListCategory) { + self.postbox = postbox + self.network = network + self.peerId = peerId + self.category = category + + self.listStateValue = ChannelMemberListState(list: [], loadingState: .ready(hasMore: true)) + self.listStatePromise = Promise(self.listStateValue) + self.loadMore() + } + + deinit { + self.loadingDisposable.dispose() + self.headUpdateDisposable.dispose() + self.headUpdateTimer?.invalidate() + } + + func loadMore() { + guard case .ready(true) = self.listStateValue.loadingState else { + return + } + + let loadCount: Int32 + if case .ready(true) = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + loadCount = initialBatchSize + } else { + loadCount = requestBatchSize + } + + self.listStateValue = self.listStateValue.withUpdatedLoadingState(.loading) + + self.loadingDisposable.set((self.loadMoreSignal(count: loadCount) + |> deliverOnMainQueue).start(next: { [weak self] members in + self?.appendMembersAndFinishLoading(members) + })) + } + + func reset() { + if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + } else { + var list = self.listStateValue.list + var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) + if list.count > Int(initialBatchSize) { + list.removeSubrange(Int(initialBatchSize) ..< list.count) + loadingState = .ready(hasMore: true) + } + + self.loadingDisposable.set(nil) + self.listStateValue = self.listStateValue.withUpdatedLoadingState(loadingState).withUpdatedList(list) + } + } + + private func loadSignal(offset: Int32, count: Int32, hash: Int32) -> Signal<[RenderedChannelParticipant]?, NoError> { + let requestCategory: ChannelMembersCategory + switch self.category { + case .recent: + requestCategory = .recent(.all) + case let .recentSearch(query): + requestCategory = .recent(.search(query)) + case .admins: + requestCategory = .admins + case .restricted: + requestCategory = .restricted(.all) + case .banned: + requestCategory = .banned(.all) + } + return channelMembers(postbox: self.postbox, network: self.network, peerId: self.peerId, category: requestCategory, offset: offset, limit: count, hash: hash) + } + + private func loadMoreSignal(count: Int32) -> Signal<[RenderedChannelParticipant], NoError> { + return self.loadSignal(offset: Int32(self.listStateValue.list.count), count: count, hash: 0) + |> map { value -> [RenderedChannelParticipant] in + return value ?? [] + } + } + + private func updateHeadMembers(_ headMembers: [RenderedChannelParticipant]?) { + if let headMembers = headMembers { + var existingIds = Set() + var list = headMembers + for member in list { + existingIds.insert(member.peer.id) + } + for member in self.listStateValue.list { + if !existingIds.contains(member.peer.id) { + list.append(member) + } + } + self.loadingDisposable.set(nil) + self.listStateValue = self.listStateValue.withUpdatedList(list) + if case .loading = self.listStateValue.loadingState { + self.loadMore() + } + } + + self.headUpdateTimer?.invalidate() + self.headUpdateTimer = nil + self.checkUpdateHead() + } + + private func appendMembersAndFinishLoading(_ members: [RenderedChannelParticipant]) { + var firstLoad = false + if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + firstLoad = true + } + var existingIds = Set() + var list = self.listStateValue.list + for member in list { + existingIds.insert(member.peer.id) + } + for member in members { + if !existingIds.contains(member.peer.id) { + list.append(member) + } + } + self.listStateValue = self.listStateValue.withUpdatedList(list).withUpdatedLoadingState(.ready(hasMore: members.count >= requestBatchSize)) + if firstLoad { + self.checkUpdateHead() + } + } + + func forceUpdateHead() { + self.headUpdateTimer = nil + self.checkUpdateHead() + } + + private func checkUpdateHead() { + if self.listStateValue.list.isEmpty { + return + } + + if self.headUpdateTimer == nil { + let headUpdateTimer = SwiftSignalKit.Timer(timeout: headUpdateTimeout, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + + var hash: UInt32 = 0 + + for i in 0 ..< min(strongSelf.listStateValue.list.count, Int(initialBatchSize)) { + let peerId = strongSelf.listStateValue.list[i].peer.id + hash = (hash &* 20261) &+ UInt32(peerId.id) + } + hash = hash % 0x7FFFFFFF + strongSelf.headUpdateDisposable.set((strongSelf.loadSignal(offset: 0, count: initialBatchSize, hash: Int32(bitPattern: hash)) + |> deliverOnMainQueue).start(next: { members in + self?.updateHeadMembers(members) + })) + }, queue: Queue.mainQueue()) + self.headUpdateTimer = headUpdateTimer + headUpdateTimer.start() + } + } + + fileprivate func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?)]) { + var list = self.listStateValue.list + var updatedList = false + for (maybePrevious, updated) in updates { + var previous: ChannelParticipant? = maybePrevious + if let participantId = maybePrevious?.peerId ?? updated?.peer.id { + inner: for participant in list { + if participant.peer.id == participantId { + previous = participant.participant + break inner + } + } + } + switch self.category { + case .admins: + if let updated = updated, let _ = updated.participant.adminInfo { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, let _ = previous.adminInfo { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .restricted: + if let updated = updated, let banInfo = updated.participant.banInfo, !banInfo.rights.flags.isEmpty && !banInfo.rights.flags.contains(.banReadMessages) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, let banInfo = previous.banInfo, !banInfo.rights.flags.isEmpty && !banInfo.rights.flags.contains(.banReadMessages) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .banned: + if let updated = updated, let banInfo = updated.participant.banInfo, banInfo.rights.flags.contains(.banReadMessages) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, let banInfo = previous.banInfo, banInfo.rights.flags.contains(.banReadMessages) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .recent: + if let updated = updated, isParticipantMember(updated.participant) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, isParticipantMember(previous) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case let .recentSearch(query): + break + default: + break + } + } + if updatedList { + self.listStateValue = self.listStateValue.withUpdatedList(list) + } + } +} + +private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategoryListContext { + private var contexts: [ChannelMemberSingleCategoryListContext] = [] + + var listStateValue: ChannelMemberListState { + return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue }) + } + + private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState { + var allReady = true + for listState in listStates { + if case .loading = listState.loadingState, listState.list.isEmpty { + allReady = false + break + } + } + if !allReady { + return ChannelMemberListState(list: [], loadingState: .loading) + } + + var list: [RenderedChannelParticipant] = [] + var existingIds = Set() + var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) + loop: for i in 0 ..< listStates.count { + for item in listStates[i].list { + if !existingIds.contains(item.peer.id) { + existingIds.insert(item.peer.id) + list.append(item) + } + } + switch listStates[i].loadingState { + case .loading: + loadingState = .loading + break loop + case let .ready(hasMore): + if hasMore { + loadingState = .ready(hasMore: true) + break loop + } + } + } + return ChannelMemberListState(list: list, loadingState: loadingState) + } + + var listState: Signal { + let signals: [Signal] = self.contexts.map { context in + return context.listState + } + return combineLatest(signals) |> map { listStates -> ChannelMemberListState in + return ChannelMemberMultiCategoryListContext.reduceListStates(listStates) + } + } + + init(postbox: Postbox, network: Network, peerId: PeerId, categories: [ChannelMemberListCategory]) { + self.contexts = categories.map { category in + return ChannelMemberSingleCategoryListContext(postbox: postbox, network: network, peerId: peerId, category: category) + } + } + + func loadMore() { + loop: for context in self.contexts { + switch context.listStateValue.loadingState { + case .loading: + break loop + case let .ready(hasMore): + if hasMore { + context.loadMore() + } + } + } + } + + func reset() { + for context in self.contexts { + context.reset() + } + } + + func forceUpdateHead() { + for context in self.contexts { + context.forceUpdateHead() + } + } + + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?)]) { + for context in self.contexts { + context.replayUpdates(updates) + } + } +} + +struct PeerChannelMemberCategoryControl { + fileprivate let key: PeerChannelMemberContextKey +} + +private final class PeerChannelMemberContextWithSubscribers { + let context: ChannelMemberCategoryListContext + private let subscribers = Bag<(ChannelMemberListState) -> Void>() + private let disposable = MetaDisposable() + private let becameEmpty: () -> Void + + private var emptyTimer: SwiftSignalKit.Timer? + + init(context: ChannelMemberCategoryListContext, becameEmpty: @escaping () -> Void) { + self.context = context + self.becameEmpty = becameEmpty + self.disposable.set((context.listState + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + for f in strongSelf.subscribers.copyItems() { + f(value) + } + } + })) + } + + deinit { + self.disposable.dispose() + self.emptyTimer?.invalidate() + } + + private func resetAndBeginEmptyTimer() { + self.context.reset() + self.emptyTimer?.invalidate() + let emptyTimer = SwiftSignalKit.Timer(timeout: emptyTimeout, repeat: false, completion: { [weak self] in + if let strongSelf = self { + if strongSelf.subscribers.isEmpty { + strongSelf.becameEmpty() + } + } + }, queue: Queue.mainQueue()) + self.emptyTimer = emptyTimer + emptyTimer.start() + } + + func subscribe(updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { + let wasEmpty = self.subscribers.isEmpty + let index = self.subscribers.add(updated) + updated(self.context.listStateValue) + if wasEmpty { + self.emptyTimer?.invalidate() + self.context.forceUpdateHead() + } + return ActionDisposable { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.subscribers.remove(index) + if strongSelf.subscribers.isEmpty { + strongSelf.resetAndBeginEmptyTimer() + } + } + } + } + } +} + +final class PeerChannelMemberCategoriesContext { + private let postbox: Postbox + private let network: Network + private let peerId: PeerId + private var becameEmpty: (Bool) -> Void + + private var contexts: [PeerChannelMemberContextKey: PeerChannelMemberContextWithSubscribers] = [:] + + init(postbox: Postbox, network: Network, peerId: PeerId, becameEmpty: @escaping (Bool) -> Void) { + self.postbox = postbox + self.network = network + self.peerId = peerId + self.becameEmpty = becameEmpty + } + + func getContext(key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + assert(Queue.mainQueue().isCurrent()) + if let current = self.contexts[key] { + return (current.subscribe(updated: updated), PeerChannelMemberCategoryControl(key: key)) + } + let context: ChannelMemberCategoryListContext + switch key { + case .recent, .recentSearch, .admins: + let mappedCategory: ChannelMemberListCategory + switch key { + case .recent: + mappedCategory = .recent + case let .recentSearch(query): + mappedCategory = .recentSearch(query) + case .admins: + mappedCategory = .admins + default: + mappedCategory = .recent + } + context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, peerId: self.peerId, category: mappedCategory) + case .restrictedAndBanned: + context = ChannelMemberMultiCategoryListContext(postbox: self.postbox, network: self.network, peerId: self.peerId, categories: [.restricted, .banned]) + } + let contextWithSubscribers = PeerChannelMemberContextWithSubscribers(context: context, becameEmpty: { [weak self] in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self { + strongSelf.contexts.removeValue(forKey: key) + } + }) + self.contexts[key] = contextWithSubscribers + return (contextWithSubscribers.subscribe(updated: updated), PeerChannelMemberCategoryControl(key: key)) + } + + func loadMore(_ control: PeerChannelMemberCategoryControl) { + assert(Queue.mainQueue().isCurrent()) + if let context = self.contexts[control.key] { + context.context.loadMore() + } + } + + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?)]) { + for (_, context) in self.contexts { + context.context.replayUpdates(updates) + } + } +} diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index b8774ee609..6005edc818 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -317,8 +317,8 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo |> filter { $0 != nil } |> take(1) |> mapToSignal { peers -> Signal in - return account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(memberId) + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(memberId) } |> deliverOnMainQueue |> mapToSignal { peer -> Signal in @@ -400,9 +400,10 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let peerView = account.viewTracker.peerView(peerId) - let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelMembers(postbox: account.postbox, network: account.network, peerId: peerId) |> map { Optional($0) }) - - peersPromise.set(peersSignal) + let (disposable, loadMoreControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, updated: { state in + peersPromise.set(.single(state.list)) + }) + actionsDisposable.add(disposable) var previousPeers: [RenderedChannelParticipant]? @@ -453,5 +454,10 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo (controller.navigationController as? NavigationController)?.pushViewController(c) } } + controller.visibleBottomContentOffsetChanged = { offset in + if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 { + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + } return controller } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 307368715c..2dd3e20ae9 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -7,6 +7,7 @@ import TelegramCore enum ChannelMembersSearchMode { case searchMembers + case banAndPromoteActions case inviteActions } @@ -30,34 +31,71 @@ private enum ChannelMembersSearchSection { } } +private enum ChannelMembersSearchContent: Equatable { + case peer(Peer) + case participant(RenderedChannelParticipant) + + static func ==(lhs: ChannelMembersSearchContent, rhs: ChannelMembersSearchContent) -> Bool { + switch lhs { + case let .peer(lhsPeer): + if case let .peer(rhsPeer) = rhs { + return lhsPeer.isEqual(rhsPeer) + } else { + return false + } + case let .participant(participant): + if case .participant(participant) = rhs { + return true + } else { + return false + } + } + } + + var peerId: PeerId { + switch self { + case let .peer(peer): + return peer.id + case let .participant(participant): + return participant.peer.id + } + } +} + private final class ChannelMembersSearchEntry: Comparable, Identifiable { let index: Int - let peer: Peer + let content: ChannelMembersSearchContent let section: ChannelMembersSearchSection - init(index: Int, peer: Peer, section: ChannelMembersSearchSection) { + init(index: Int, content: ChannelMembersSearchContent, section: ChannelMembersSearchSection) { self.index = index - self.peer = peer + self.content = content self.section = section } var stableId: PeerId { - return self.peer.id + return self.content.peerId } static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { - return lhs.index == rhs.index && arePeersEqual(lhs.peer, rhs.peer) && lhs.section == rhs.section + return lhs.index == rhs.index && lhs.content == rhs.content && lhs.section == rhs.section } static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { - let peer = self.peer - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: self.peer, chatPeer: self.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in - peerSelected(peer) - }) + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void) -> ListViewItem { + switch self.content { + case let .peer(peer): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + peerSelected(peer, nil) + }) + case let .participant(participant): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: participant.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + peerSelected(participant.peer, participant) + }) + } } } struct ChannelMembersSearchContainerTransition { @@ -67,7 +105,7 @@ struct ChannelMembersSearchContainerTransition { let isSearching: Bool } -private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ChannelMembersSearchContainerTransition { +private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void) -> ChannelMembersSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -79,7 +117,7 @@ private func channelMembersSearchContainerPreparedRecentTransition(from fromEntr final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account - private let openPeer: (Peer) -> Void + private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private let mode: ChannelMembersSearchMode private let dimNode: ASDisplayNode @@ -96,7 +134,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, peerId: PeerId, mode: ChannelMembersSearchMode, openPeer: @escaping (Peer) -> Void) { + init(account: Account, peerId: PeerId, mode: ChannelMembersSearchMode, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { self.account = account self.openPeer = openPeer self.mode = mode @@ -121,22 +159,32 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChannelMembersSearchEntry]?, NoError> in if let query = query, !query.isEmpty { - let foundGroupMembers: Signal<[Peer], NoError> + let foundGroupMembers: Signal<[RenderedChannelParticipant], NoError> let foundMembers: Signal<[RenderedChannelParticipant], NoError> switch mode { - case .searchMembers: - foundGroupMembers = searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peerId, query: query) + case .searchMembers, .banAndPromoteActions: + foundGroupMembers = Signal { subscriber in + let (disposable, listControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, searchQuery: query, updated: { state in + // FIXME: remove and test list expansion bug + if case .ready = state.loadingState { + subscriber.putNext(state.list) + subscriber.putCompletion() + } + }) + return disposable + } |> runOn(Queue.mainQueue()) foundMembers = .single([]) case .inviteActions: foundGroupMembers = .single([]) - foundMembers = channelMembers(postbox: account.postbox, network: account.network, peerId: peerId, filter: .search(query)) + foundMembers = channelMembers(postbox: account.postbox, network: account.network, peerId: peerId, category: .recent(.search(query))) + |> map { $0 ?? [] } } let foundContacts: Signal<[Peer], NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> switch mode { - case .inviteActions: + case .inviteActions, .banAndPromoteActions: foundContacts = account.postbox.searchContacts(query: query.lowercased()) foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) @@ -153,17 +201,17 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var index = 0 - for peer in foundGroupMembers { - if !existingPeerIds.contains(peer.id) { - existingPeerIds.insert(peer.id) + for participant in foundGroupMembers { + if !existingPeerIds.contains(participant.peer.id) { + existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection switch mode { - case .inviteActions: + case .inviteActions, .banAndPromoteActions: section = .members case .searchMembers: section = .none } - entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: section)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant), section: section)) index += 1 } } @@ -173,12 +221,12 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection switch mode { - case .inviteActions: + case .inviteActions, .banAndPromoteActions: section = .members case .searchMembers: section = .none } - entries.append(ChannelMembersSearchEntry(index: index, peer: participant.peer, section: section)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant), section: section)) index += 1 } } @@ -186,7 +234,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod for peer in foundContacts { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .contacts)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts)) index += 1 } } @@ -195,7 +243,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .global)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global)) index += 1 } } @@ -204,7 +252,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .global)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global)) index += 1 } } diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index b09fef7d53..b9d5c8b708 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -9,7 +9,7 @@ final class ChannelMembersSearchController: ViewController { private let account: Account private let peerId: PeerId - private let openPeer: (Peer) -> Void + private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private var presentationData: PresentationData @@ -19,7 +19,7 @@ final class ChannelMembersSearchController: ViewController { return self.displayNode as! ChannelMembersSearchControllerNode } - init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void) { + init(account: Account, peerId: PeerId, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { self.account = account self.peerId = peerId self.openPeer = openPeer @@ -32,6 +32,12 @@ final class ChannelMembersSearchController: ViewController { self.title = self.presentationData.strings.Channel_Members_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.scrollToTop() + } + } } required public init(coder aDecoder: NSCoder) { @@ -47,9 +53,9 @@ final class ChannelMembersSearchController: ViewController { self.controllerNode.requestDeactivateSearch = { [weak self] in self?.deactivateSearch(animated: true) } - self.controllerNode.requestOpenPeerFromSearch = { [weak self] peer in + self.controllerNode.requestOpenPeerFromSearch = { [weak self] peer, participant in self?.dismiss() - self?.openPeer(peer) + self?.openPeer(peer, participant) } self.displayNodeDidLoad() diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 02422deb54..050d4c3323 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -5,6 +5,99 @@ import Postbox import TelegramCore import SwiftSignalKit +private final class ChannelMembersSearchInteraction { + let activateSearch: () -> Void + let openPeer: (Peer, RenderedChannelParticipant?) -> Void + + init(activateSearch: @escaping () -> Void, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { + self.activateSearch = activateSearch + self.openPeer = openPeer + } +} + +private enum ChannelMembersSearchEntryId: Hashable { + case search + case peer(PeerId) +} + +private enum ChannelMembersSearchEntry: Comparable, Identifiable { + case search + case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing) + + var stableId: ChannelMembersSearchEntryId { + switch self { + case .search: + return .search + case let .peer(peer): + return .peer(peer.1.peer.id) + } + } + + static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { + switch lhs { + case .search: + if case .search = rhs { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsParticipant, lhsEditing): + if case .peer(lhsIndex, lhsParticipant, lhsEditing) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { + switch lhs { + case .search: + if case .search = rhs { + return false + } else { + return true + } + case let .peer(lhsPeer): + if case let .peer(rhsPeer) = rhs { + return lhsPeer.0 < rhsPeer.0 + } else { + return false + } + } + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelMembersSearchInteraction) -> ListViewItem { + switch self { + case .search: + return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + interaction.activateSearch() + }) + case let .peer(_, participant, editing): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: editing, index: nil, header: nil, action: { _ in + interaction.openPeer(participant.peer, participant) + }) + } + } +} + +private struct ChannelMembersSearchTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let initial: Bool +} + +private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, to toEntries: [ChannelMembersSearchEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelMembersSearchInteraction) -> ChannelMembersSearchTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries ?? [], rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + + return ChannelMembersSearchTransition(deletions: deletions, insertions: insertions, updates: updates, initial: fromEntries == nil) +} + class ChannelMembersSearchControllerNode: ASDisplayNode { private let account: Account private let peerId: PeerId @@ -12,17 +105,20 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { let listNode: ListView var navigationBar: NavigationBar? + private var enqueuedTransitions: [ChannelMembersSearchTransition] = [] + private(set) var searchDisplayController: SearchDisplayController? private var containerLayout: (ContainerViewLayout, CGFloat)? var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? - var requestOpenPeerFromSearch: ((Peer) -> Void)? + var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)? var themeAndStrings: (PresentationTheme, PresentationStrings) private var disposable: Disposable? + private var listControl: PeerChannelMemberCategoryControl? init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId) { self.account = account @@ -41,32 +137,38 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { self.addSubnode(self.listNode) - self.disposable = (channelMembers(postbox: account.postbox, network: account.network, peerId: peerId) - |> deliverOnMainQueue).start(next: { [weak self] participants in - if let strongSelf = self { - var items: [ListViewItem] = [] - items.append(ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { - if let strongSelf = self { - strongSelf.requestActivateSearch?() - } - })) - - for participant in participants { - items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in - if let strongSelf = self { - strongSelf.requestOpenPeerFromSearch?(peer) - } - })) - } - - var insertItems: [ListViewInsertItem] = [] - for i in 0 ..< items.count { - insertItems.append(ListViewInsertItem(index: i, previousIndex: nil, item: items[i], directionHint: nil)) - } - - strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: insertItems, updateIndicesAndItems: [], options: [], updateOpaqueState: nil) - } - }) + let interaction = ChannelMembersSearchInteraction(activateSearch: { [weak self] in + self?.requestActivateSearch?() + }, openPeer: { [weak self] peer, participant in + self?.requestOpenPeerFromSearch?(peer, participant) + }) + + let previousEntries = Atomic<[ChannelMembersSearchEntry]?>(value: nil) + let (disposable, loadMoreControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, updated: { [weak self] state in + guard let strongSelf = self else { + return + } + var entries: [ChannelMembersSearchEntry] = [] + entries.append(.search) + + var index = 0 + for participant in state.list { + entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false))) + index += 1 + } + + let previous = previousEntries.swap(entries) + + strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, account: account, theme: theme, strings: strings, interaction: interaction)) + }) + self.disposable = disposable + self.listControl = loadMoreControl + + self.listNode.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + } } deinit { @@ -79,6 +181,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.containerLayout != nil self.containerLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.input]) @@ -116,6 +219,12 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } } func activateSearch() { @@ -135,10 +244,8 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChannelMembersSearchContainerNode(account: self.account, peerId: self.peerId, mode: .inviteActions, openPeer: { [weak self] peer in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peer) - } + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChannelMembersSearchContainerNode(account: self.account, peerId: self.peerId, mode: .banAndPromoteActions, openPeer: { [weak self] peer, participant in + self?.requestOpenPeerFromSearch?(peer, participant) }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() @@ -175,4 +282,36 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { completion?() }) } + + private func enqueueTransition(_ transition: ChannelMembersSearchTransition) { + enqueuedTransitions.append(transition) + + if self.containerLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + let options = ListViewDeleteAndInsertOptions() + if transition.initial { + //options.insert(.Synchronous) + //options.insert(.LowLatency) + } else { + //options.insert(.AnimateTopItemPosition) + //options.insert(.AnimateCrossfade) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } } diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index e4a81be553..39c710bf2b 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -504,7 +504,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa text = presentationData.strings.Channel_Username_InvalidTooShort } case .invalidCharacters: - text = presentationData.strings.Channel_Username_InvalidTaken + text = presentationData.strings.Channel_Username_InvalidCharacters } case let .availability(availability): switch availability { @@ -716,8 +716,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: peersDisablingAddressNameAssignment.set(.single([])) })) }, copyPrivateLink: { - let _ = (account.postbox.modify { modifier -> String? in - if let cachedData = modifier.getPeerCachedData(peerId: peerId) { + let _ = (account.postbox.transaction { transaction -> String? in + if let cachedData = transaction.getPeerCachedData(peerId: peerId) { if let cachedData = cachedData as? CachedChannelData { return cachedData.exportedInvitation?.link } else if let cachedData = cachedData as? CachedGroupData { @@ -748,8 +748,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: })) } }, sharePrivateLink: { - let _ = (account.postbox.modify { modifier -> String? in - if let cachedData = modifier.getPeerCachedData(peerId: peerId) { + let _ = (account.postbox.transaction { transaction -> String? in + if let cachedData = transaction.getPeerCachedData(peerId: peerId) { if let cachedData = cachedData as? CachedChannelData { return cachedData.exportedInvitation?.link } else if let cachedData = cachedData as? CachedGroupData { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 6bfcc8902d..2e48c491e9 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -97,7 +97,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private let enqueueMediaMessageDisposable = MetaDisposable() private var resolvePeerByNameDisposable: MetaDisposable? - private let editingMessage = ValuePromise(false, ignoreRepeated: true) + private let editingMessage = ValuePromise(nil, ignoreRepeated: true) private let startingBot = ValuePromise(false, ignoreRepeated: true) private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) private let searching = ValuePromise(false, ignoreRepeated: true) @@ -167,6 +167,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private weak var mediaRecordingModeTooltipController: TooltipController? private var screenCaptureEventsDisposable: Disposable? + private let chatAdditionalDataDisposable = MetaDisposable() public init(account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false)) { self.account = account @@ -471,7 +472,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let _ = (strongSelf.account.postbox.loadedPeerWithId(strongSelf.account.peerId) |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil), replyToMessageId: nil, localGroupingKey: nil)]) } }) } @@ -567,6 +568,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) + }, navigationController: { [weak self] in + return self?.navigationController as? NavigationController }, presentGlobalOverlayController: { [weak self] controller, arguments in self?.presentInGlobalOverlay(controller, with: arguments) }, callPeer: { [weak self] peerId in @@ -577,8 +580,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.account.telegramApplicationContext.navigateToCurrentCall?() } else { let presentationData = strongSelf.presentationData - let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in - return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { peer, current in if let strongSelf = self, let peer = peer, let current = current { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), 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: { @@ -835,7 +838,35 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatTitleView?.titleContent = .peer(peerView) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) } + var wasGroupChannel: Bool? + if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + wasGroupChannel = true + } else { + wasGroupChannel = false + } + } + var isGroupChannel: Bool? + if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + isGroupChannel = true + } else { + isGroupChannel = false + } + } strongSelf.peerView = peerView + if wasGroupChannel != isGroupChannel { + if let isGroupChannel = isGroupChannel, isGroupChannel { + let (recentDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) + let (adminsDisposable, _) = strongSelf.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: peerView.peerId, updated: { _ in }) + let disposable = DisposableSet() + disposable.add(recentDisposable) + disposable.add(adminsDisposable) + strongSelf.chatAdditionalDataDisposable.set(disposable) + } else { + strongSelf.chatAdditionalDataDisposable.set(nil) + } + } if strongSelf.isNodeLoaded { strongSelf.chatDisplayNode.peerView = peerView } @@ -1135,6 +1166,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.canReadHistoryDisposable?.dispose() self.networkStateDisposable?.dispose() self.screenCaptureEventsDisposable?.dispose() + self.chatAdditionalDataDisposable.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -1222,12 +1254,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } if let readStateData = combinedInitialData.readStateData { if case let .peer(peerId) = strongSelf.chatLocation, let peerReadStateData = readStateData[peerId], let notificationSettings = peerReadStateData.notificationSettings { - var globalRemainingUnreadCount = peerReadStateData.totalUnreadCount - if !notificationSettings.isRemovedFromTotalUnreadCount { - globalRemainingUnreadCount -= peerReadStateData.unreadCount + var globalRemainingUnreadChatCount = peerReadStateData.totalUnreadChatCount + if !notificationSettings.isRemovedFromTotalUnreadCount && peerReadStateData.unreadCount > 0 { + globalRemainingUnreadChatCount -= 1 } - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + if globalRemainingUnreadChatCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)" } else { strongSelf.navigationItem.badge = "" } @@ -1445,93 +1477,18 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, { $0.updatedInterfaceState(f) }) } + self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] animated, f in + self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, f) + } + self.chatDisplayNode.displayAttachmentMenu = { [weak self] in - guard let strongSelf = self else { - return - } - let _ = (strongSelf.account.postbox.modify { modifier -> GeneratedMediaStoreSettings in - let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings - return entry ?? GeneratedMediaStoreSettings.defaultSettings - } - |> deliverOnMainQueue).start(next: { [weak self] settings in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - strongSelf.chatDisplayNode.dismissInput() - - let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - legacyController.bind(controller: navigationController) - - let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { - self?.presentMediaPicker(fileMode: false) - }, openCamera: { cameraView, menuController in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in - self?.enqueueMediaMessages(signals: signals) - }) - } - }, openFileGallery: { - self?.presentFileMediaPickerOptions() - }, openMap: { - self?.presentMapPicker() - }, openContacts: { - if let strongSelf = self { - let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.Contacts_Title }) - strongSelf.chatDisplayNode.dismissInput() - 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) - |> take(1) - strongSelf.controllerNavigationDisposable.set((peer |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let user = peer as? TelegramUser, let phone = user.phone, !phone.isEmpty { - let media = TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId, localGroupingKey: nil) - strongSelf.sendMessages([message]) - } - })) - } - })) - } - }, sendMessagesWithSignals: { [weak self] signals in - self?.enqueueMediaMessages(signals: signals) - }, selectRecentlyUsedInlineBot: { [weak self] peer in - if let strongSelf = self, let addressName = peer.addressName { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in - return .text - }) - }) - } - }) - controller.didDismiss = { [weak legacyController] _ in - legacyController?.dismiss() - } - controller.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - - strongSelf.present(legacyController, in: .window(.root)) - controller.present(in: emptyController, sourceView: nil, animated: true) - } - }) + self?.presentAttachmentMenu(editingMessage: false) } let postbox = self.account.postbox self.chatDisplayNode.displayPasteMenu = { [weak self] images in - let _ = (postbox.modify { modifier -> GeneratedMediaStoreSettings in - let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + let _ = (postbox.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in @@ -1610,6 +1567,19 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }, setupEditMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { + guard let messageId = messageId else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState { + $0.withUpdatedEditMessage(nil) + } + state = state.updatedEditMessageState(nil) + return state + }) + strongSelf.editMessageDisposable.set(nil) + + return + } if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var updated = state.updatedInterfaceState { @@ -1622,20 +1592,42 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(message.text, entities: entities)), disableUrlPreview: nil)) } - updated = updated.updatedInputMode({ _ in - return .text - }) + var hasOriginalMedia = false for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { updated = updated.updatedEditingUrlPreview((content.url, webpage)) - break + } else if media is TelegramMediaImage || media is TelegramMediaFile { + hasOriginalMedia = true } } + updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(hasOriginalMedia: hasOriginalMedia, media: nil)) + updated = updated.updatedInputMode({ _ in + return .text + }) + return updated }) - //strongSelf.chatDisplayNode.ensureInputViewFocused() } } + }, setupEditMessageMedia: { [weak self] in + if let strongSelf = self, case .peer = strongSelf.chatLocation, let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { + let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } |> deliverOnMainQueue).start(next: { message in + guard let strongSelf = self, let message = message else { + return + } + /*var isFile = false + for media in message.media { + if let file = media as? TelegramMediaFile { + if !(file.isVideo || file.isInstantVideo) { + isFile = true + } + } + }*/ + strongSelf.presentAttachmentMenu(editingMessage: true) + }) + } }, beginMessageSelection: { [weak self] messageIds in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }) @@ -1685,8 +1677,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) strongController.dismiss() } else { - let _ = (strongSelf.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedForwardMessageIds(forwardMessageIds) } else { @@ -1732,8 +1724,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) strongController.dismiss() } else { - let _ = (strongSelf.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedForwardMessageIds(forwardMessageIds) } else { @@ -1763,10 +1755,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { - let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in + let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in var messages: [Message] = [] for id in selectedIds { - if let message = modifier.getMessage(id) { + if let message = transaction.getMessage(id) { messages.append(message) } } @@ -1804,7 +1796,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } let editingMessage = strongSelf.editingMessage - editingMessage.set(true) let text = trimChatInputText(editMessage.inputState.inputText) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1812,11 +1803,30 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin entitiesAttribute = TextEntitiesMessageAttribute(entities: entities) } - strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: text.string, entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ - editingMessage.set(false) - })).start(completed: { + let media: RequestEditMessageMedia + if let editMedia = strongSelf.presentationInterfaceState.editMessageState?.media { + media = .update(editMedia) + } else { + media = .keep + } + + strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: text.string, media: media + , entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ + editingMessage.set(nil) + })).start(next: { result in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) }) + switch result { + case let .progress(value): + editingMessage.set(value) + case .done: + editingMessage.set(nil) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) + state = state.updatedEditMessageState(nil) + return state + }) + } } })) } @@ -2005,6 +2015,52 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.deleteMediaRecording() }, sendRecordedMedia: { [weak self] in self?.sendMediaRecording() + }, displayRestrictedInfo: { [weak self] subject in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let bannedRights = (peer as? TelegramChannel)?.bannedRights { + let banDescription: String + switch subject { + case .stickers: + banDescription = strongSelf.presentationInterfaceState.strings.Group_ErrorSendRestrictedStickers + case .mediaRecording: + if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: strongSelf.presentationInterfaceState.strings, timeFormat: .regular)).0 + } else { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia + } + } + if strongSelf.recordingModeFeedback == nil { + strongSelf.recordingModeFeedback = HapticFeedback() + strongSelf.recordingModeFeedback?.prepareError() + } + + strongSelf.recordingModeFeedback?.error() + + let rect: CGRect? + switch subject { + case .stickers: + rect = strongSelf.chatDisplayNode.frameForStickersButton() + case .mediaRecording: + rect = strongSelf.chatDisplayNode.frameForInputActionButton() + } + + if let tooltipController = strongSelf.mediaRecordingModeTooltipController { + tooltipController.text = banDescription + } else if let rect = rect { + let tooltipController = TooltipController(text: banDescription) + strongSelf.mediaRecordingModeTooltipController = tooltipController + tooltipController.dismissed = { [weak tooltipController] in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController { + strongSelf.mediaRecordingModeTooltipController = nil + } + } + strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { + if let strongSelf = self { + return (strongSelf.chatDisplayNode, rect) + } + return nil + })) + } + } }, switchMediaRecordingMode: { [weak self] in if let strongSelf = self { if strongSelf.recordingModeFeedback == nil { @@ -2188,9 +2244,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let stickerFile = stickerFile { let postbox = strongSelf.account.postbox let network = strongSelf.account.network - let _ = (strongSelf.account.postbox.modify { modifier -> Signal in - if getIsStickerSaved(modifier: modifier, fileId: stickerFile.fileId) { - removeSavedSticker(modifier: modifier, mediaId: stickerFile.fileId) + let _ = (strongSelf.account.postbox.transaction { transaction -> Signal in + if getIsStickerSaved(transaction: transaction, fileId: stickerFile.fileId) { + removeSavedSticker(transaction: transaction, mediaId: stickerFile.fileId) return .complete() } else { return addSavedSticker(postbox: postbox, network: network, file: stickerFile) @@ -2257,32 +2313,32 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin switch self.chatLocation { case let .peer(peerId): - let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(.filtered)]) + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(.filtered, .messages)]) let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 - var totalCount: Int32 = 0 + var totalChatCount: Int32 = 0 if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { if let count = view.count(for: .peer(peerId)) { unreadCount = count } - if let count = view.count(for: .total(.filtered)) { - totalCount = count + if let count = view.count(for: .total(.filtered, .chats)) { + totalChatCount = count } } strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings { - var globalRemainingUnreadCount = totalCount - if !notificationSettings.isRemovedFromTotalUnreadCount { - globalRemainingUnreadCount -= unreadCount + var globalRemainingUnreadChatCount = totalChatCount + if !notificationSettings.isRemovedFromTotalUnreadCount && unreadCount > 0 { + globalRemainingUnreadChatCount -= 1 } - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + if globalRemainingUnreadChatCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)" } else { strongSelf.navigationItem.badge = "" } @@ -2315,11 +2371,11 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if foundAllPeers { return .single(cachedResult) } else { - return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in + return postbox.transaction { transaction -> [(Peer, PeerInputActivity)] in var result: [(Peer, PeerInputActivity)] = [] var peerCache: [PeerId: Peer] = [:] for (peerId, activity) in activities { - if let peer = modifier.getPeer(peerId) { + if let peer = transaction.getPeer(peerId) { result.append((peer, activity)) peerCache[peerId] = peer } @@ -2345,7 +2401,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } })) case let .group(groupId): - let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(.filtered)]) + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(.filtered, .messages)]) //let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { @@ -2356,7 +2412,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let count = view.count(for: .group(groupId)) { unreadCount = count } - if let count = view.count(for: .total(.filtered)) { + if let count = view.count(for: .total(.filtered, .messages)) { totalCount = count } } @@ -2611,6 +2667,18 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return inScopeResult(previousResult) }) } + + if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + if case .contextRequest = query { + let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(postbox: self.account.postbox) + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self, !value { + let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(postbox: strongSelf.account.postbox).start() + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + } + } } } @@ -2644,7 +2712,30 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? - self.urlPreviewQueryState = (updatedUrlPreviewUrl, (updatedUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in + let linkPreviews: Signal + if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + linkPreviews = interactiveChatLinkPreviewsEnabled(postbox: self.account.postbox, displayAlert: { [weak self] f in + if let strongSelf = self { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + f.f(true) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: { + f.f(false) + })]), in: .window(.root)) + } + }) + } else { + if let bannedRights = (self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banEmbedLinks) { + linkPreviews = .single(false) + } else { + linkPreviews = .single(true) + } + } + self.urlPreviewQueryState = (updatedUrlPreviewUrl, (combineLatest(updatedUrlPreviewSignal, linkPreviews) |> deliverOnMainQueue).start(next: { [weak self] (result, enabled) in + var result = result + if !enabled { + result = { _ in return nil } + } if let strongSelf = self { if Thread.isMainThread && inScope { inScope = false @@ -2670,7 +2761,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } - if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText.string, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { + let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.hasOriginalMedia ?? false + let editingUrlPreviewText: String? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText.string + if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { self.editingUrlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? @@ -2839,13 +2932,140 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } - private func presentFileMediaPickerOptions() { + private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { + if let message = messages.first, case let .message(desc) = message, let media = desc.media { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedEditMessageState(ChatEditInterfaceMessageState(hasOriginalMedia: true, media: media)) + return state + }) + } + } + + private func editMessageMediaWithLegacySignals(_ signals: [Any]) { + guard case let .peer(peerId) = self.chatLocation else { + return + } + + let _ = (legacyAssetPickerEnqueueMessages(account: self.account, peerId: peerId, signals: signals) + |> deliverOnMainQueue).start(next: { [weak self] messages in + self?.editMessageMediaWithMessages(messages) + }) + } + + private func presentAttachmentMenu(editingMessage: Bool) { + let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + strongSelf.chatDisplayNode.dismissInput() + + if !editingMessage, let bannedRights = (peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendMedia) { + let banDescription: String + if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: strongSelf.presentationInterfaceState.strings, timeFormat: .regular)).0 + } else { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia + } + + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: banDescription), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + self?.presentMapPicker(editingMessage: false) + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + self?.presentContactPicker() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window(.root)) + + return + } + + let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + legacyController.enableSizeClassSignal = true + let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { + self?.presentMediaPicker(fileMode: false, completion: { signals in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals) + } else { + self?.enqueueMediaMessages(signals: signals) + } + }) + }, openCamera: { cameraView, menuController in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, saveCapturedPhotos: settings.storeEditedPhotos, sendMessagesWithSignals: { signals in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals!) + } else { + self?.enqueueMediaMessages(signals: signals) + } + }) + } + }, openFileGallery: { + self?.presentFileMediaPickerOptions(editingMessage: editingMessage) + }, openMap: { + self?.presentMapPicker(editingMessage: editingMessage) + }, openContacts: { + self?.presentContactPicker() + }, sendMessagesWithSignals: { [weak self] signals in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals!) + } else { + self?.enqueueMediaMessages(signals: signals) + } + }, selectRecentlyUsedInlineBot: { [weak self] peer in + if let strongSelf = self, let addressName = peer.addressName { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in + return .text + }) + }) + } + }) + controller.didDismiss = { [weak legacyController] _ in + legacyController?.dismiss() + } + controller.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + + strongSelf.present(legacyController, in: .window(.root)) + controller.present(in: emptyController, sourceView: nil, animated: true) + }) + } + + private func presentFileMediaPickerOptions(editingMessage: Bool) { let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.presentMediaPicker(fileMode: true) + strongSelf.presentMediaPicker(fileMode: true, completion: { signals in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals) + } else { + self?.enqueueMediaMessages(signals: signals) + } + }) } }), ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in @@ -2873,14 +3093,19 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } if !messages.isEmpty { + if editingMessage { + strongSelf.editMessageMediaWithMessages(messages) + + } else { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - strongSelf.sendMessages(messages) + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + strongSelf.sendMessages(messages) + } } } })) @@ -2897,9 +3122,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.present(actionSheet, in: .window(.root)) } - private func presentMediaPicker(fileMode: Bool) { - let _ = (self.account.postbox.modify { modifier -> GeneratedMediaStoreSettings in - let entry = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings + private func presentMediaPicker(fileMode: Bool, completion: @escaping ([Any]) -> Void) { + let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in @@ -2918,7 +3143,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin controller.completionBlock = { [weak legacyController] signals in if let strongSelf = self, let legacyController = legacyController { legacyController.dismiss() - strongSelf.enqueueMediaMessages(signals: signals) + completion(signals!) } } controller.dismissalBlock = { [weak legacyController] in @@ -2935,7 +3160,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } - private func presentMapPicker() { + private func presentMapPicker(editingMessage: Bool) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -2945,8 +3170,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } else { selfPeerId = self.account.peerId } - let _ = (self.account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(selfPeerId) + let _ = (self.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(selfPeerId) } |> deliverOnMainQueue).start(next: { [weak self] selfPeer in guard let strongSelf = self, let selfPeer = selfPeer else { @@ -2954,9 +3179,16 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(legacyLocationPickerController(selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue in - if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.present(legacyLocationPickerController(account: strongSelf.account, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue in + guard let strongSelf = self else { + return + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil), replyToMessageId: replyMessageId, localGroupingKey: nil) + + if editingMessage { + strongSelf.editMessageMediaWithMessages([message]) + } else { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { @@ -2964,11 +3196,41 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil), replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } - }, sendLiveLocation: { [weak self] coordinate, period in - if let strongSelf = self { + }, sendLiveLocation: { [weak self] coordinate, period in + guard let strongSelf = self else { + return + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: replyMessageId, localGroupingKey: nil) + if editingMessage { + strongSelf.editMessageMediaWithMessages([message]) + } else { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + strongSelf.sendMessages([message]) + } + }, theme: strongSelf.presentationData.theme), in: .window(.root)) + }) + } + + private func presentContactPicker() { + let contactsController = ContactSelectionController(account: self.account, title: { $0.Contacts_Title }) + self.chatDisplayNode.dismissInput() + self.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { [weak self] peerId in + if let strongSelf = self, let peerId = peerId { + let peer = strongSelf.account.postbox.loadedPeerWithId(peerId) + |> take(1) + strongSelf.controllerNavigationDisposable.set((peer |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let user = peer as? TelegramUser, let phone = user.phone, !phone.isEmpty { + let media = TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -2977,11 +3239,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: replyMessageId, localGroupingKey: nil) + let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.sendMessages([message]) } - }, theme: strongSelf.presentationData.theme), in: .window(.root)) - }) + })) + } + })) } private func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] { @@ -3507,8 +3770,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin })) case let .chat(textInputState, messageId): if let textInputState = textInputState { - let _ = (self.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (self.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { @@ -3549,8 +3812,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) strongController.dismiss() } else { - let _ = (strongSelf.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { @@ -3731,7 +3994,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin @available(iOSApplicationExtension 9.0, *) public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { if previewingContext.sourceView === (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view { - if let peer = self.presentationInterfaceState.renderedPeer?.peer { + if let peer = self.presentationInterfaceState.renderedPeer?.peer, peer.smallProfileImage != nil { let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) @@ -3848,10 +4111,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let chatLocation = self.chatLocation let data = Atomic(value: nil) let semaphore = DispatchSemaphore(value: 0) - let _ = self.account.postbox.modify({ modifier -> Void in + let _ = self.account.postbox.transaction({ transaction -> Void in switch chatLocation { case let .peer(peerId): - let _ = data.swap(PreviewActionsData(notificationSettings: modifier.getPeerNotificationSettings(peerId), peer: modifier.getPeer(peerId))) + let _ = data.swap(PreviewActionsData(notificationSettings: transaction.getPeerNotificationSettings(peerId), peer: transaction.getPeer(peerId))) case .group: let _ = data.swap(PreviewActionsData(notificationSettings: nil, peer: nil)) } @@ -4004,8 +4267,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) if actions.contains(3) { - let _ = strongSelf.account.postbox.modify({ modifier -> Void in - modifier.removeAllMessagesWithAuthor(peerId, authorId: author.id) + let _ = strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.removeAllMessagesWithAuthor(peerId, authorId: author.id) }).start() let _ = clearAuthorHistory(account: strongSelf.account, peerId: peerId, memberId: author.id).start() } else if actions.contains(0) { diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index d7a1052e27..69ca530e63 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -58,6 +58,7 @@ public final class ChatControllerInteraction { let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void let openMessageShareMenu: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void + let navigationController: () -> NavigationController? let presentGlobalOverlayController: (ViewController, Any?) -> Void let callPeer: (PeerId) -> Void let longTap: (ChatControllerInteractionLongTapAction) -> Void @@ -74,7 +75,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -96,6 +97,7 @@ public final class ChatControllerInteraction { self.updateInputMode = updateInputMode self.openMessageShareMenu = openMessageShareMenu self.presentController = presentController + self.navigationController = navigationController self.presentGlobalOverlayController = presentGlobalOverlayController self.callPeer = callPeer self.longTap = longTap diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index cc5fd735ac..f6592fbd0f 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -104,6 +104,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _ in } + var requestUpdateInterfaceState: (Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _ in } var sendMessages: ([EnqueueMessage]) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var displayPasteMenu: ([UIImage]) -> Void = { _ in } @@ -627,6 +628,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) + accessoryPanelNode.updateState(size: CGSize(width: layout.size.width, height: layout.size.height), interfaceState: self.chatPresentationInterfaceState) + if accessoryPanelNode !== self.accessoryPanelNode { dismissedAccessoryPanelNode = self.accessoryPanelNode self.accessoryPanelNode = accessoryPanelNode @@ -644,7 +647,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedEditMessage(nil) }) + strongSelf.interfaceInteraction?.setupEditMessage(nil) } else if let _ = accessoryPanelNode as? WebpagePreviewAccessoryPanelNode { strongSelf.dismissUrlPreview() } @@ -780,7 +783,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { listInsets.left += 6.0 listInsets.right += 6.0 listInsets.top += 6.0 - listInsets.bottom += 6.0 + containerInsets.bottom += 6.0 + //listInsets.bottom += 6.0 } } @@ -829,8 +833,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if displayTopDimNode { var topInset = listInsets.bottom + UIScreenPixel - if let _ = titleAccessoryPanelHeight { - topInset -= UIScreenPixel + if let titleAccessoryPanelHeight = titleAccessoryPanelHeight { + if expandTopDimNode { + topInset -= titleAccessoryPanelHeight + } else { + topInset -= UIScreenPixel + } } let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) @@ -1331,6 +1339,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return nil } + func frameForStickersButton() -> CGRect? { + if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { + return textInputPanelNode.frameForStickersButton().flatMap { + return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) + } + } + return nil + } + var isTextInputPanelActive: Bool { return self.inputPanelNode is ChatTextInputPanelNode } @@ -1511,12 +1528,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { switch item.content { - case let .message(message, _, _): + case let .message(message, _, _, _): if message.stableId == stableId { resultItemNode = itemNode } case let .group(messages): - for (message, _, _) in messages { + for (message, _, _, _) in messages { if message.stableId == stableId { resultItemNode = itemNode break diff --git a/TelegramUI/ChatEditInterfaceMessageState.swift b/TelegramUI/ChatEditInterfaceMessageState.swift new file mode 100644 index 0000000000..ec2ee86c3f --- /dev/null +++ b/TelegramUI/ChatEditInterfaceMessageState.swift @@ -0,0 +1,27 @@ +import Foundation +import Postbox +import TelegramCore + +final class ChatEditInterfaceMessageState: Equatable { + let hasOriginalMedia: Bool + let media: Media? + + init(hasOriginalMedia: Bool, media: Media?) { + self.hasOriginalMedia = hasOriginalMedia + self.media = media + } + + static func ==(lhs: ChatEditInterfaceMessageState, rhs: ChatEditInterfaceMessageState) -> Bool { + if lhs.hasOriginalMedia != rhs.hasOriginalMedia { + return false + } + if let lhsMedia = lhs.media, let rhsMedia = rhs.media { + if !lhsMedia.isEqual(rhsMedia) { + return false + } + } else if (lhs.media != nil) != (rhs.media != nil) { + return false + } + return true + } +} diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift index 358b4e47bf..1cbf118ca5 100644 --- a/TelegramUI/ChatEmptyItem.swift +++ b/TelegramUI/ChatEmptyItem.swift @@ -131,7 +131,7 @@ final class ChatEmptyItemNode: ListViewItemNode { if let iconImage = iconImage { imageSize = iconImage.size } - let imageSpacing: CGFloat = 10.0 + let imageSpacing: CGFloat = 18.0 let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) diff --git a/TelegramUI/ChatEmptyNode.swift b/TelegramUI/ChatEmptyNode.swift index 92be9f959f..a3e05fd26c 100644 --- a/TelegramUI/ChatEmptyNode.swift +++ b/TelegramUI/ChatEmptyNode.swift @@ -39,15 +39,16 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod let insets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + let iconVerticalInset: CGFloat = 14.0 let iconSize = self.iconNode.image?.size ?? CGSize() let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let spacing: CGFloat = 8.0 + let spacing: CGFloat = 26.0 let contentWidth = max(iconSize.width, textSize.width) - let contentHeight = iconSize.height + spacing + textSize.height + let contentHeight = iconVerticalInset + iconSize.height + spacing + textSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) - let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - iconSize.width) / 2.0), y: contentRect.minY), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - iconSize.width) / 2.0), y: contentRect.minY + iconVerticalInset), size: iconSize) transition.updateFrame(node: self.iconNode, frame: iconFrame) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: iconFrame.maxY + spacing), size: textSize)) diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index 0cd084ffcc..67d40b4572 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -2,10 +2,21 @@ import Foundation import Postbox import TelegramCore -func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData) -> [ChatHistoryEntry] { var entries: [ChatHistoryEntry] = [] + var adminIds = Set() + if case let .peer(peerId) = location, peerId.namespace == Namespaces.Peer.CloudChannel { + for additionalEntry in view.additionalData { + if case let .cacheEntry(id, data) = additionalEntry { + if id == cachedChannelAdminIdsEntryId(peerId: peerId), let data = data as? CachedChannelAdminIds { + adminIds = data.ids + } + break + } + } + } - var groupBucket: [(Message, Bool, ChatHistoryMessageSelection)] = [] + var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, Bool)] = [] for entry in view.entries { switch entry { case let .HoleEntry(hole, _): @@ -17,6 +28,11 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B entries.append(.HoleEntry(hole, presentationData.theme, presentationData.strings)) } case let .MessageEntry(message, read, _, monthLocation): + var isAdmin = false + if let author = message.author { + isAdmin = adminIds.contains(author.id) + } + if groupMessages { if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) @@ -29,7 +45,7 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } else { selection = .none } - groupBucket.append((message, read, selection)) + groupBucket.append((message, read, selection, isAdmin)) } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -37,7 +53,7 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } else { selection = .none } - entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection)) + entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection, isAdmin)) } } else { let selection: ChatHistoryMessageSelection @@ -46,7 +62,7 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } else { selection = .none } - entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection)) + entries.append(.MessageEntry(message, presentationData, read, monthLocation, selection, isAdmin)) } } } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index 17470d5131..9731265abb 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -25,8 +25,8 @@ public enum ChatHistoryMessageSelection: Equatable { enum ChatHistoryEntry: Identifiable, Comparable { case HoleEntry(MessageHistoryHole, PresentationTheme, PresentationStrings) - case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryMonthLocation?, ChatHistoryMessageSelection) - case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection)], ChatPresentationData) + case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryMonthLocation?, ChatHistoryMessageSelection, Bool) + case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, Bool)], ChatPresentationData) case UnreadEntry(MessageIndex, PresentationTheme, PresentationStrings) case ChatInfoEntry(String, PresentationTheme, PresentationStrings) case EmptyChatInfoEntry(PresentationTheme, PresentationStrings, MessageTags?) @@ -36,7 +36,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { switch self { case let .HoleEntry(hole, _, _): return UInt64(hole.stableId) | ((UInt64(1) << 40)) - case let .MessageEntry(message, _, _, _, _): + case let .MessageEntry(message, _, _, _, _, _): return UInt64(message.stableId) | ((UInt64(2) << 40)) case let .MessageGroupEntry(groupInfo, _, _): return UInt64(groupInfo.stableId) | ((UInt64(2) << 40)) @@ -55,7 +55,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { switch self { case let .HoleEntry(hole, _, _): return hole.maxIndex - case let .MessageEntry(message, _, _, _, _): + case let .MessageEntry(message, _, _, _, _, _): return MessageIndex(message) case let .MessageGroupEntry(_, messages, _): return MessageIndex(messages[messages.count - 1].0) @@ -78,9 +78,9 @@ enum ChatHistoryEntry: Identifiable, Comparable { } else { return false } - case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection): + case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsIsAdmin): switch rhs { - case let .MessageEntry(rhsMessage, rhsPresentationData, rhsRead, _, rhsSelection) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + case let .MessageEntry(rhsMessage, rhsPresentationData, rhsRead, _, rhsSelection, rhsIsAdmin) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: if lhsPresentationData !== rhsPresentationData { return false } @@ -110,6 +110,9 @@ enum ChatHistoryEntry: Identifiable, Comparable { if lhsSelection != rhsSelection { return false } + if lhsIsAdmin != rhsIsAdmin { + return false + } return true default: return false @@ -117,8 +120,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { case let .MessageGroupEntry(lhsGroupInfo, lhsMessages, lhsPresentationData): if case let .MessageGroupEntry(rhsGroupInfo, rhsMessages, rhsPresentationData) = rhs, lhsGroupInfo == rhsGroupInfo, lhsPresentationData === rhsPresentationData, lhsMessages.count == rhsMessages.count { for i in 0 ..< lhsMessages.count { - let (lhsMessage, lhsRead, lhsSelection) = lhsMessages[i] - let (rhsMessage, rhsRead, rhsSelection) = rhsMessages[i] + let (lhsMessage, lhsRead, lhsSelection, lhsIsAdmin) = lhsMessages[i] + let (rhsMessage, rhsRead, rhsSelection, rhsIsAdmin) = rhsMessages[i] if lhsMessage.id != rhsMessage.id { return false @@ -161,6 +164,9 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } } + if lhsIsAdmin != rhsIsAdmin { + return false + } } return true diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index b9e5b85db0..a701b75090 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -18,7 +18,7 @@ struct ChatHistoryGridViewTransition { private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeInsertItem] { return entries.map { entry -> GridNodeInsertItem in switch entry.entry { - case let .MessageEntry(message, _, _, _, _): + case let .MessageEntry(message, _, _, _, _, _): return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) case .MessageGroupEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) @@ -37,7 +37,7 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeUpdateItem] { return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { - case let .MessageEntry(message, _, _, _, _): + case let .MessageEntry(message, _, _, _, _, _): return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) case .MessageGroupEntry: return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) @@ -124,7 +124,7 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI var topOffsetWithinMonth: Int = 0 if let lastEntry = transition.historyView.filteredEntries.last { switch lastEntry { - case let .MessageEntry(_, _, _, monthLocation, _): + case let .MessageEntry(_, _, _, monthLocation, _, _): if let monthLocation = monthLocation { topOffsetWithinMonth = Int(monthLocation.indexInMonth) } @@ -246,7 +246,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, account: account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, presentationData: chatPresentationData) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) @@ -307,7 +307,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries where message.id == id { + for case let .MessageEntry(message, _, _, _, _, _) in historyView.filteredEntries where message.id == id { return message } } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 59a8b29e74..067d09b7eb 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -40,7 +40,7 @@ enum ChatHistoryViewUpdateType { public struct ChatHistoryCombinedInitialReadStateData { public let unreadCount: Int32 - public let totalUnreadCount: Int32 + public let totalUnreadChatCount: Int32 public let notificationSettings: PeerNotificationSettings? } @@ -120,7 +120,7 @@ struct ChatHistoryListViewTransition { private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) { var overall: MessageIndex? for i in (indexRange.0 ... indexRange.1).reversed() { - if case let .MessageEntry(message, _, _, _, _) = entries[i] { + if case let .MessageEntry(message, _, _, _, _, _) = entries[i] { if overall == nil { overall = MessageIndex(message) } @@ -143,11 +143,11 @@ private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case let .MessageEntry(message, presentationData, read, _, selection): + case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } @@ -188,11 +188,11 @@ private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, c private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case let .MessageEntry(message, presentationData, read, _, selection): + case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } @@ -360,6 +360,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { additionalData.append(.cachedPeerData(peerId)) additionalData.append(.cachedPeerDataMessages(peerId)) additionalData.append(.peerNotificationSettings(peerId)) + if peerId.namespace == Namespaces.Peer.CloudChannel { + additionalData.append(.cacheEntry(cachedChannelAdminIdsEntryId(peerId: peerId))) + } } additionalData.append(.totalUnreadState) @@ -418,7 +421,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { reverse = reverseValue } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { @@ -492,8 +495,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { case .peer: let _ = applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: messageIndex).start() case let .group(groupId): - let _ = account.postbox.modify({ modifier -> Void in - modifier.applyGroupFeedInteractiveReadMaxIndex(groupId: groupId, index: messageIndex) + let _ = account.postbox.transaction({ transaction -> Void in + transaction.applyGroupFeedInteractiveReadMaxIndex(groupId: groupId, index: messageIndex) }).start() } } @@ -532,7 +535,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageIdsWithUnseenPersonalMention: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { switch historyView.filteredEntries[i] { - case let .MessageEntry(message, _, _, _, _): + case let .MessageEntry(message, _, _, _, _, _): var hasUnconsumedMention = false var hasUnsonsumedContent = false if message.tags.contains(.unseenPersonalMessage) { @@ -555,7 +558,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { messageIdsWithUnseenPersonalMention.append(message.id) } case let .MessageGroupEntry(_, messages, _): - for (message, _, _) in messages { + for (message, _, _, _) in messages { var hasUnconsumedMention = false var hasUnsonsumedContent = false if message.tags.contains(.unseenPersonalMessage) { @@ -691,7 +694,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var index = historyView.filteredEntries.count - 1 loop: for entry in historyView.filteredEntries { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { currentMessage = message break loop } else if case let .MessageGroupEntry(_, messages, _) = entry { @@ -732,7 +735,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var index = 0 for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { return message } } @@ -740,7 +743,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries { + for case let .MessageEntry(message, _, _, _, _, _) in historyView.filteredEntries { return message } } @@ -750,12 +753,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { for entry in historyView.filteredEntries { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { if message.id == id { return message } } else if case let .MessageGroupEntry(_, messages, _) = entry { - for (message, _, _) in messages { + for (message, _, _, _) in messages { if message.id == id { return message } @@ -769,12 +772,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func messageGroupInCurrentHistoryView(_ id: MessageId) -> [Message]? { if let historyView = self.historyView { for entry in historyView.filteredEntries { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { if message.id == id { return [message] } } else if case let .MessageGroupEntry(_, messages, _) = entry { - for (message, _, _) in messages { + for (message, _, _, _) in messages { if message.id == id { return messages.map { $0.0 } } @@ -788,12 +791,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func forEachMessageInCurrentHistoryView(_ f: (Message) -> Bool) { if let historyView = self.historyView { for entry in historyView.filteredEntries { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { if !f(message) { return } } else if case let .MessageGroupEntry(_, messages, _) = entry { - for (message, _, _) in messages { + for (message, _, _, _) in messages { if !f(message) { return } @@ -987,7 +990,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var index = 0 loop: for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _, _, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _, _) = entry { if index != 0 || historyView.originalView.laterId != nil { currentMessage = message } @@ -1069,13 +1072,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let historyView = self.historyView { loop: for i in 0 ..< historyView.filteredEntries.count { switch historyView.filteredEntries[i] { - case let .MessageEntry(message, presentationData, read, _, selection): + case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): if message.id == id { let index = historyView.filteredEntries.count - 1 - i let item: ListViewItem switch self.mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection)) + item = ChatMessageItem(presentationData: presentationData, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: search) } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 5229b6061b..ebf9c4e294 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -200,7 +200,7 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL case let .peer(peerId): if let combinedReadStates = view.combinedReadStates { if case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId] { - readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadState.filteredCounters.messageCount, notificationSettings: notificationSettings) + readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadChatCount: totalUnreadState.filteredCounters.chatCount, notificationSettings: notificationSettings) } } case .group: diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index fdef208452..2c1e68555d 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Postbox import TelegramCore -private enum ChatMediaGalleryThumbnail: Equatable { +enum ChatMediaGalleryThumbnail: Equatable { case image(TelegramMediaImage) case video(TelegramMediaFile) diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index 7f9256372c..182d1dfb49 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -29,6 +29,21 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return nil } + if let bannedRights = (chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendInline) { + switch inputQueryResult { + case .stickers, .contextRequestResult: + if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { + return currentPanel + } else { + let panel = DisabledContextResultsChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panel.interfaceInteraction = interfaceInteraction + return panel + } + default: + break + } + } + switch inputQueryResult { case let .stickers(results): if !results.isEmpty { @@ -53,14 +68,16 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return panel } case let .emojis(results): - if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode { - currentPanel.updateResults(results) - return currentPanel - } else { - let panel = EmojisChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - panel.interfaceInteraction = interfaceInteraction - panel.updateResults(results) - return panel + if !results.isEmpty { + if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode { + currentPanel.updateResults(results) + return currentPanel + } else { + let panel = EmojisChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panel.interfaceInteraction = interfaceInteraction + panel.updateResults(results) + return panel + } } case let .mentions(peers): if !peers.isEmpty { diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index c8096270c0..361eb6c1ad 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -19,7 +19,7 @@ struct PossibleContextQueryTypes: OptionSet { static let mention = PossibleContextQueryTypes(rawValue: (1 << 2)) static let command = PossibleContextQueryTypes(rawValue: (1 << 3)) static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4)) - static let stickerSearch = PossibleContextQueryTypes(rawValue: (1 << 5)) + static let emojiSearch = PossibleContextQueryTypes(rawValue: (1 << 5)) } private func makeScalar(_ c: Character) -> Character { @@ -34,6 +34,17 @@ private let slashScalar = "/" as UnicodeScalar private let dotsScalar = ":" as UnicodeScalar private let alphanumerics = CharacterSet.alphanumerics +private func scalarCanPrependQueryControl(_ c: UnicodeScalar?) -> Bool { + if let c = c { + if c == " " || c == "\n" || c == "." || c == "," { + return true + } + return false + } else { + return true + } +} + func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] { if inputState.selectionRange.count != 0 { return [] @@ -94,36 +105,48 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> return [(NSRange(location: 0, length: inputLength), [.emoji], nil)] } - var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .stickerSearch]) + var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .emojiSearch]) var definedType = false while true { + var previousC: UnicodeScalar? + if index != 0 { + previousC = UnicodeScalar(inputString.character(at: index - 1)) + } if let c = UnicodeScalar(inputString.character(at: index)) { if c == spaceScalar || c == newlineScalar { possibleTypes = [] } else if c == hashScalar { - possibleTypes = possibleTypes.intersection([.hashtag]) - definedType = true - index += 1 - possibleQueryRange = NSRange(location: index, length: maxIndex - index) + if scalarCanPrependQueryControl(previousC) { + possibleTypes = possibleTypes.intersection([.hashtag]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + } break } else if c == atScalar { - possibleTypes = possibleTypes.intersection([.mention]) - definedType = true - index += 1 - possibleQueryRange = NSRange(location: index, length: maxIndex - index) + if scalarCanPrependQueryControl(previousC) { + possibleTypes = possibleTypes.intersection([.mention]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + } break - } else if c == slashScalar { - possibleTypes = possibleTypes.intersection([.command]) - definedType = true - index += 1 - possibleQueryRange = NSRange(location: index, length: maxIndex - index) + } else if c == slashScalar { + if scalarCanPrependQueryControl(previousC) { + possibleTypes = possibleTypes.intersection([.command]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + } break } else if c == dotsScalar { - possibleTypes = possibleTypes.intersection([.stickerSearch]) - definedType = true - index += 1 - possibleQueryRange = NSRange(location: index, length: maxIndex - index) + if scalarCanPrependQueryControl(previousC) { + possibleTypes = possibleTypes.intersection([.emojiSearch]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + } break } } @@ -164,8 +187,8 @@ func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInter } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { let additionalString = inputString.substring(with: additionalStringRange) result.append(.contextRequest(addressName: query, query: additionalString)) - } else if possibleTypes == [.stickerSearch], !query.isEmpty { - result.append(.stickerSearch(query)) + } else if possibleTypes == [.emojiSearch], !query.isEmpty { + result.append(.emojiSearch(query)) } } return result @@ -209,15 +232,21 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) } - if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, canSendMessagesToPeer(peer) { - accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) + var stickersEnabled = true + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { + if case .broadcast = peer.info, canSendMessagesToPeer(peer) { + accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) + } + if let bannedRights = peer.bannedRights, bannedRights.flags.contains(.banSendStickers) { + stickersEnabled = false + } } if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser { if let _ = peer.botInfo { accessoryItems.append(.commands) } } - accessoryItems.append(.stickers) + accessoryItems.append(.stickers(stickersEnabled)) if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup { accessoryItems.append(.inputButtons) } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 3b887023ce..649f943da3 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -141,11 +141,17 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let _ = attribute as? InlineBotMessageAttribute { hasUneditableAttributes = true break + } else if let _ = attribute as? AutoremoveTimeoutMessageAttribute { + hasUneditableAttributes = true + break } } if message.forwardInfo != nil { hasUneditableAttributes = true } + if message.groupingKey != nil { + hasUneditableAttributes = true + } for media in message.media { if let file = media as? TelegramMediaFile { @@ -156,6 +162,9 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } else if let _ = media as? TelegramMediaContact { hasUneditableAttributes = true break + } else if let _ = media as? TelegramMediaExpiredContent { + hasUneditableAttributes = true + break } } @@ -170,10 +179,10 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: var loadStickerSaveStatusSignal: Signal = .single(nil) if loadStickerSaveStatus != nil { - loadStickerSaveStatusSignal = account.postbox.modify { modifier -> Bool? in + loadStickerSaveStatusSignal = account.postbox.transaction { transaction -> Bool? in var starStatus: Bool? if let loadStickerSaveStatus = loadStickerSaveStatus { - if getIsStickerSaved(modifier: modifier, fileId: loadStickerSaveStatus) { + if getIsStickerSaved(transaction: transaction, fileId: loadStickerSaveStatus) { starStatus = true } else { starStatus = false @@ -343,7 +352,7 @@ struct ChatAvailableMessageActions { } func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal { - return postbox.modify { modifier -> ChatAvailableMessageActions in + return postbox.transaction { transaction -> ChatAvailableMessageActions in var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:] var banPeer: Peer? var hadBanPeerId = false @@ -353,7 +362,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } if id.peerId == accountPeerId { optionsMap[id]!.insert(.deleteLocally) - } else if let peer = modifier.getPeer(id.peerId), let message = modifier.getMessage(id) { + } else if let peer = transaction.getPeer(id.peerId), let message = transaction.getMessage(id) { if let channel = peer as? TelegramChannel { if channel.hasAdminRights(.canBanUsers), case .group = channel.info { if message.flags.contains(.Incoming) { diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index cb5e80c207..cfb4a24e9b 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -15,7 +15,18 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return [:] } - let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) + let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState).filter({ query in + if chatPresentationInterfaceState.editMessageState != nil { + switch query { + case .contextRequest, .command, .emoji: + return false + default: + return true + } + } else { + return true + } + }) var updates: [ChatPresentationInputQueryKind: ChatContextQueryUpdate] = [:] @@ -233,7 +244,7 @@ private func updatedContextQueryResultStateForQuery(account: Account, peer: Peer } return signal |> then(contextBot) - case let .stickerSearch(query): + case let .emojiSearch(query): let foundEmojis: Signal<[(String, String)], NoError> = Signal { subscriber in var result: [(String, String)] = [] for entry in TGEmojiSuggestions.suggestions(forQuery: query.lowercased()) { diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index dbf1f9699f..5e135f190a 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -96,6 +96,17 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState break } } else if let channel = peer as? TelegramChannel { + if let bannedRights = channel.bannedRights, bannedRights.flags.contains(.banSendMessages) { + if let currentPanel = currentPanel as? ChatRestrictedInputPanelNode { + return currentPanel + } else { + let panel = ChatRestrictedInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + switch channel.participationStatus { case .kicked: if let currentPanel = currentPanel as? DeleteChatInputPanelNode { diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 76c9189eb9..7a6e56246c 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -308,8 +308,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { @objc func deleteButtonPressed() { if let currentMessage = self.currentMessage { - let _ = (self.account.postbox.modify { modifier -> [Message] in - return modifier.getMessageGroup(currentMessage.id) ?? [] + let _ = (self.account.postbox.transaction { transaction -> [Message] in + return transaction.getMessageGroup(currentMessage.id) ?? [] } |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { if messages.count == 1 { @@ -432,8 +432,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { @objc func actionButtonPressed() { if let currentMessage = self.currentMessage { - let _ = (self.account.postbox.modify { modifier -> [Message] in - return modifier.getMessageGroup(currentMessage.id) ?? [] + let _ = (self.account.postbox.transaction { transaction -> [Message] in + return transaction.getMessageGroup(currentMessage.id) ?? [] } |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -458,7 +458,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { } if messages.count == 1 { - let shareController = ShareController(account: strongSelf.account, subject: .messages([currentMessage]), saveToCameraRoll: saveToCameraRoll) + var subject: ShareControllerSubject = ShareControllerSubject.messages(messages) + for m in messages[0].media { + if let image = m as? TelegramMediaImage { + subject = .image(image.representations) + } + } + let shareController = ShareController(account: strongSelf.account, subject: subject, saveToCameraRoll: true) strongSelf.controllerInteraction?.presentController(shareController, nil) } else { var singleText = presentationData.strings.Media_ShareItem(1) diff --git a/TelegramUI/ChatLinkPreview.swift b/TelegramUI/ChatLinkPreview.swift new file mode 100644 index 0000000000..9880de7118 --- /dev/null +++ b/TelegramUI/ChatLinkPreview.swift @@ -0,0 +1,31 @@ +import Foundation +import Postbox +import SwiftSignalKit + +final class InteractiveChatLinkPreviewsResult { + let f: (Bool) -> Void + + init(_ f: @escaping (Bool) -> Void) { + self.f = f + } +} + +func interactiveChatLinkPreviewsEnabled(postbox: Postbox, displayAlert: @escaping (InteractiveChatLinkPreviewsResult) -> Void) -> Signal { + return ApplicationSpecificNotice.getSecretChatLinkPreviews(postbox: postbox) + |> mapToSignal { value -> Signal in + if let value = value { + return .single(value) + } else { + return Signal { subscriber in + Queue.mainQueue().async { + displayAlert(InteractiveChatLinkPreviewsResult({ result in + let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(postbox: postbox, value: result).start() + subscriber.putNext(result) + subscriber.putCompletion() + })) + } + return EmptyDisposable + } + } + } +} diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 32ba2eebff..e828231f1b 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -4,6 +4,28 @@ import SwiftSignalKit import Display import TelegramCore +private let tabImageNone = UIImage(bundleImageName: "Chat List/Tabs/IconChats")?.precomposed() +private let tabImageUp = tabImageNone.flatMap({ image in + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.translateBy(x: 0.0, y: 7.0) + let _ = try? drawSvgPath(context, path: "M14.6557321,9.04533883 C14.9642504,8.81236784 15.4032142,8.87361104 15.6361852,9.18212936 C15.8691562,9.49064768 15.807913,9.9296115 15.4993947,10.1625825 L11.612306,13.0978342 C11.3601561,13.2882398 11.0117095,13.2861239 10.7618904,13.0926701 L6.97141581,10.1574184 C6.66574952,9.92071787 6.60984175,9.48104267 6.84654232,9.17537638 C7.08324289,8.86971009 7.5229181,8.81380232 7.82858438,9.05050289 L11.1958257,11.658013 L14.6557321,9.04533883 Z ") + }) +}) +private let tabImageUnread = tabImageNone.flatMap({ image in + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.translateBy(x: 0.0, y: 7.0) + let _ = try? drawSvgPath(context, path: "M14.6557321,12.0977948 L11.1958257,9.48512064 L7.82858438,12.0926307 C7.5229181,12.3293313 7.08324289,12.2734235 6.84654232,11.9677572 C6.60984175,11.662091 6.66574952,11.2224158 6.97141581,10.9857152 L10.7618904,8.05046348 C11.0117095,7.85700968 11.3601561,7.85489378 11.612306,8.04529942 L15.4993947,10.9805511 C15.807913,11.2135221 15.8691562,11.6524859 15.6361852,11.9610043 C15.4032142,12.2695226 14.9642504,12.3307658 14.6557321,12.0977948 Z ") + }) +}) + public class ChatListController: TelegramController, UIViewControllerPreviewingDelegate { private let account: Account private let controlsHistoryPreload: Bool @@ -17,8 +39,12 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } private let titleView: NetworkStatusTitleView + private var proxyUnavailableTooltipController: TooltipController? + private var didShowProxyUnavailableTooltipController = false + private var titleDisposable: Disposable? private var badgeDisposable: Disposable? + private var badgeIconDisposable: Disposable? private var dismissSearchOnDisappear = false @@ -49,8 +75,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false, hasProxy: false, connectsViaProxy: false) self.navigationItem.titleView = self.titleView self.tabBarItem.title = self.presentationData.strings.DialogList_Title - self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") - self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChats") + self.tabBarItem.image = tabImageNone + self.tabBarItem.selectedImage = tabImageNone self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) @@ -62,9 +88,10 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in - if let strongSelf = self { - strongSelf.chatListDisplayNode.chatListNode.scrollToLatest() - } + self?.chatListDisplayNode.chatListNode.scrollToPosition(.top) + } + self.scrollToTopWithTabBar = { [weak self] in + self?.chatListDisplayNode.chatListNode.scrollToPosition(.auto) } let hasProxy = account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) @@ -82,28 +109,57 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.titleDisposable = (combineLatest(account.networkState |> deliverOnMainQueue, hasProxy |> deliverOnMainQueue)).start(next: { [weak self] state, proxy in if let strongSelf = self { let (hasProxy, connectsViaProxy) = proxy + var checkProxy = false switch state { case .waitingForNetwork: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) - case .connecting: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Connecting, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) + case let .connecting(proxy): + var text = strongSelf.presentationData.strings.State_Connecting + if let proxy = proxy, proxy.hasConnectionIssues { + checkProxy = true + } + strongSelf.titleView.title = NetworkStatusTitle(text: text, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) case .updating: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) case .online: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.DialogList_Title, activity: false, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) } + if checkProxy { + if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil { + strongSelf.didShowProxyUnavailableTooltipController = true + let tooltipController = TooltipController(text: "The proxy may be unavailable. Try selecting another one.", timeout: 60.0, dismissByTapOutside: true) + strongSelf.proxyUnavailableTooltipController = tooltipController + tooltipController.dismissed = { [weak tooltipController] in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.proxyUnavailableTooltipController === tooltipController { + strongSelf.proxyUnavailableTooltipController = nil + } + } + strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { + if let strongSelf = self, let rect = strongSelf.titleView.proxyButtonRect() { + return (strongSelf.titleView, rect.insetBy(dx: 0.0, dy: -4.0)) + } + return nil + })) + } + } else { + strongSelf.didShowProxyUnavailableTooltipController = false + if let proxyUnavailableTooltipController = strongSelf.proxyUnavailableTooltipController { + strongSelf.proxyUnavailableTooltipController = nil + proxyUnavailableTooltipController.dismiss() + } + } } }) self.badgeDisposable = (renderedTotalUnreadCount(postbox: account.postbox) |> deliverOnMainQueue).start(next: { [weak self] count in if let strongSelf = self { - if count == 0 { + if count.0 == 0 { strongSelf.tabBarItem.badgeValue = "" } else { - if count > 1000 && false { - strongSelf.tabBarItem.badgeValue = "\(count / 1000)K" + if count.0 > 1000 && false { + strongSelf.tabBarItem.badgeValue = "\(count.0 / 1000)K" } else { - strongSelf.tabBarItem.badgeValue = "\(count)" + strongSelf.tabBarItem.badgeValue = "\(count.0)" } } } @@ -118,15 +174,15 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.titleView.toggleIsLocked = { [weak self] in if let strongSelf = self { - let _ = strongSelf.account.postbox.modify({ modifier -> Void in - var data = modifier.getAccessChallengeData() + let _ = strongSelf.account.postbox.transaction({ transaction -> Void in + var data = transaction.getAccessChallengeData() if data.isLockable { if data.autolockDeadline != 0 { data = data.withUpdatedAutolockDeadline(0) } else { data = data.withUpdatedAutolockDeadline(nil) } - modifier.setAccessChallengeData(data) + transaction.setAccessChallengeData(data) } }).start() } @@ -161,6 +217,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.openMessageFromSearchDisposable.dispose() self.titleDisposable?.dispose() self.badgeDisposable?.dispose() + self.badgeIconDisposable?.dispose() self.passcodeDisposable.dispose() self.presentationDataDisposable?.dispose() } @@ -217,8 +274,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in if let strongSelf = self { - let _ = (strongSelf.account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) + let _ = (strongSelf.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) @@ -266,6 +323,16 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId in if let strongSelf = self { if let navigationController = strongSelf.navigationController as? NavigationController { + /*let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(postbox: strongSelf.account.postbox) + |> deliverOnMainQueue).start(next: { value in + if !value { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: "The proxy you are using displays a sponsored channel in your chat list.", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + if let strongSelf = self { + let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(postbox: strongSelf.account.postbox).start() + } + })]), in: .window(.root)) + } + })*/ navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId)) strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) } @@ -302,9 +369,9 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.chatListDisplayNode.requestOpenPeerFromSearch = { [weak self] peer in if let strongSelf = self { - let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in + let storedPeer = strongSelf.account.postbox.transaction { transaction -> Void in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in return updatedPeer }) } @@ -348,6 +415,22 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } } + self.badgeIconDisposable = (self.chatListDisplayNode.chatListNode.scrollToTopOption + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] option in + guard let strongSelf = self else { + return + } + switch option { + case .none: + strongSelf.tabBarItem.selectedImage = tabImageNone + case .top: + strongSelf.tabBarItem.selectedImage = tabImageUp + case .unread: + strongSelf.tabBarItem.selectedImage = tabImageUnread + } + }) + self.displayNodeDidLoad() } @@ -502,7 +585,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD previewingContext.sourceRect = sourceRect } switch item.content { - case let .peer(_, peer, _, _, _, _, _): + case let .peer(_, peer, _, _, _, _, _, _): if peer.peerId.namespace != Namespaces.Peer.SecretChat { let chatController = ChatController(account: self.account, chatLocation: .peer(peer.peerId), mode: .standard(previewing: true)) chatController.canReadHistory.set(false) diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 13082e5020..691ab0f2c8 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -104,7 +104,7 @@ class ChatListControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChatListSearchContainerNode(account: self.account, onlyWriteable: false, groupId: self.groupId, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChatListSearchContainerNode(account: self.account, filter: [], groupId: self.groupId, openPeer: { [weak self] peer in self?.requestOpenPeerFromSearch?(peer) }, openRecentPeerOptions: { [weak self] peer in self?.requestOpenRecentPeerOptions?(peer) diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index b955d7fe2d..334c5112e2 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -7,12 +7,12 @@ import SwiftSignalKit import TelegramCore enum ChatListItemContent { - case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?) + case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool) case groupReference(groupId: PeerGroupId, message: Message?, topPeers: [Peer], counters: GroupReferenceUnreadCounters) var chatLocation: ChatLocation { switch self { - case let .peer(_, peer, _, _, _, _, _): + case let .peer(_, peer, _, _, _, _, _, _): return .peer(peer.peerId) case let .groupReference(groupId, _, _, _): return .group(groupId) @@ -95,7 +95,7 @@ class ChatListItem: ListViewItem { func selected(listView: ListView) { switch self.content { - case let .peer(message, peer, _, _, _, _, _): + case let .peer(message, peer, _, _, _, _, _, _): if let message = message { self.interaction.messageSelected(message) } else if let peer = peer.peers[peer.peerId] { @@ -146,6 +146,8 @@ private let unmuteIcon = UIImage(bundleImageName: "Chat List/RevealActionUnmuteI private let deleteIcon = UIImage(bundleImageName: "Chat List/RevealActionDeleteIcon")?.precomposed() private let groupIcon = UIImage(bundleImageName: "Chat List/RevealActionGroupIcon")?.precomposed() private let ungroupIcon = UIImage(bundleImageName: "Chat List/RevealActionUngroupIcon")?.precomposed() +private let readIcon = UIImage(bundleImageName: "Chat List/RevealActionReadIcon")?.precomposed() +private let unreadIcon = UIImage(bundleImageName: "Chat List/RevealActionUnreadIcon")?.precomposed() private enum RevealOptionKey: Int32 { case pin @@ -155,6 +157,7 @@ private enum RevealOptionKey: Int32 { case delete case group case ungroup + case toggleMarkedUnread } private let itemHeight: CGFloat = 76.0 @@ -188,6 +191,16 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem return options } +private func leftRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isUnread: Bool) -> [ItemListRevealOption] { + var options: [ItemListRevealOption] = [] + if isUnread { + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: "Read", icon: readIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + } else { + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: "Unread", icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + } + return options +} + private let separatorHeight = 1.0 / UIScreen.main.scale private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 26.0)! @@ -308,7 +321,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var peer: Peer? switch item.content { - case let .peer(message, peerValue, _, _, _, _, _): + case let .peer(message, peerValue, _, _, _, _, _, _): if let message = message { peer = messageMainPeer(message) } else { @@ -396,30 +409,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let message: Message? let itemPeer: RenderedPeer let combinedReadState: CombinedPeerReadState? - let unreadCount: (count: Int32, muted: Bool) + let unreadCount: (count: Int32, unread: Bool, muted: Bool) let notificationSettings: PeerNotificationSettings? let embeddedState: PeerChatListEmbeddedInterfaceState? let summaryInfo: ChatListMessageTagSummaryInfo let inputActivities: [(Peer, PeerInputActivity)]? let isPeerGroup: Bool + let isAd: Bool var multipleAvatarsApply: ((Bool) -> MultipleAvatarsNode)? switch item.content { - case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue): + case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue, isAdValue): message = messageValue itemPeer = peerValue combinedReadState = combinedReadStateValue - if let combinedReadState = combinedReadState { - unreadCount = (combinedReadState.count, notificationSettingsValue?.isRemovedFromTotalUnreadCount ?? false) + if let combinedReadState = combinedReadState, !isAdValue { + unreadCount = (combinedReadState.count, combinedReadState.isUnread, notificationSettingsValue?.isRemovedFromTotalUnreadCount ?? false) } else { - unreadCount = (0, false) + unreadCount = (0, false, false) + } + if isAdValue { + notificationSettings = nil + } else { + notificationSettings = notificationSettingsValue } - notificationSettings = notificationSettingsValue embeddedState = embeddedStateValue summaryInfo = summaryInfoValue inputActivities = inputActivitiesValue isPeerGroup = false + isAd = isAdValue case let .groupReference(_, messageValue, topPeersValue, counters): if let messageValue = messageValue { itemPeer = RenderedPeer(message: messageValue) @@ -435,12 +454,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { isPeerGroup = true multipleAvatarsApply = multipleAvatarsLayout(item.account, topPeersValue, CGSize(width: 60.0, height: 60.0)) if counters.unreadCount > 0 { - unreadCount = (counters.unreadCount + counters.unreadMutedCount, false) + let count = counters.unreadCount + counters.unreadMutedCount + unreadCount = (count, count > 0, false) } else if counters.unreadMutedCount > 0 { - unreadCount = (counters.unreadMutedCount, true) + unreadCount = (counters.unreadMutedCount, counters.unreadMutedCount > 0, true) } else{ - unreadCount = (0, false) + unreadCount = (0, false, false) } + isAd = false } let theme = item.presentationData.theme.chatList @@ -471,10 +492,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var reorderInset: CGFloat = 0.0 if item.editing { let sizeAndApply = editableControlLayout(itemHeight, item.presentationData.theme, isPeerGroup) - editableControlSizeAndApply = sizeAndApply + if !isAd { + editableControlSizeAndApply = sizeAndApply + } editingOffset = sizeAndApply.0.width - if item.index.pinningIndex != nil { + if item.index.pinningIndex != nil && !isAd { let sizeAndApply = reorderControlLayout(itemHeight, item.presentationData.theme) reorderControlSizeAndApply = sizeAndApply reorderInset = sizeAndApply.0.width @@ -505,11 +528,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { if let peer = peer as? TelegramChannel, case .broadcast = peer.info { } else { - peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(or: item.presentationData.strings.Peer_DeletedUser) + peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings) } } else if case .groupReference = item.content { if let messagePeer = itemPeer.chatMainPeer { - peerText = messagePeer.displayTitle(or: item.presentationData.strings.Peer_DeletedUser) + peerText = messagePeer.displayTitle(strings: item.presentationData.strings) } } @@ -524,7 +547,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { case .peer: if peer?.id == item.account.peerId { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor) - } else if let displayTitle = peer?.displayTitle(or: item.presentationData.strings.Peer_DeletedUser) { + } else if let displayTitle = peer?.displayTitle(strings: item.presentationData.strings) { titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) } case .groupReference: @@ -540,7 +563,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp, timeFormat: item.presentationData.timeFormat) - dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) + if isAd { + dateAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_AdLabel, font: dateFont, textColor: theme.dateTextColor) + } else { + dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) + } if let message = message, message.author?.id == account.peerId && !hasDraft { if message.flags.isSending { @@ -554,7 +581,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if unreadCount.count > 0 { + if unreadCount.unread { if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount.count == 1 { } else { let badgeTextColor: UIColor @@ -565,7 +592,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme) badgeTextColor = theme.unreadBadgeActiveTextColor } - badgeAttributedString = NSAttributedString(string: "\(unreadCount.count)", font: badgeFont, textColor: badgeTextColor) + badgeAttributedString = NSAttributedString(string: unreadCount.count > 0 ? "\(unreadCount.count)" : " ", font: badgeFont, textColor: badgeTextColor) } } @@ -574,7 +601,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let totalMentionCount = tagSummaryCount - actionsSummaryCount if totalMentionCount > 0 { currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme) - } else if item.index.pinningIndex != nil && currentBadgeBackgroundImage == nil { + } else if item.index.pinningIndex != nil && !isAd && currentBadgeBackgroundImage == nil { currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme) } @@ -665,6 +692,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: itemHeight), insets: insets) let peerRevealOptions: [ItemListRevealOption] + let peerLeftRevealOptions: [ItemListRevealOption] switch item.content { case .peer: var hasPeerGroupId: Bool? @@ -679,10 +707,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { isPinned = item.index.pinningIndex != nil } - if item.enableContextActions { + if item.enableContextActions && !isAd { peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: item.account.peerId != item.index.messageIndex.id.peerId ? (currentMutedIconImage != nil) : nil, hasPeerGroupId: hasPeerGroupId, canDelete: true) + peerLeftRevealOptions = leftRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isUnread: unreadCount.unread) } else { peerRevealOptions = [] + peerLeftRevealOptions = [] } case .groupReference: let isPinned = item.index.pinningIndex != nil @@ -692,6 +722,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { peerRevealOptions = [] } + peerLeftRevealOptions = [] } return (layout, { [weak self] animated in @@ -985,7 +1016,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions(peerRevealOptions) + strongSelf.setRevealOptions((left: peerLeftRevealOptions, right: peerRevealOptions)) strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) } }) @@ -1145,6 +1176,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true) case RevealOptionKey.ungroup.rawValue: item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, false) + case RevealOptionKey.toggleMarkedUnread.rawValue: + item.interaction.togglePeerMarkedUnread(item.index.messageIndex.id.peerId) + close = false default: break } @@ -1161,4 +1195,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } return false } + + func flashHighlight() { + if self.highlightedBackgroundNode.supernode == nil { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + self.highlightedBackgroundNode.alpha = 0.0 + } + self.highlightedBackgroundNode.layer.removeAllAnimations() + self.highlightedBackgroundNode.layer.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseOut, duration: 0.3, delay: 0.7, completion: { [weak self] _ in + self?.updateIsHighlighted(transition: .immediate) + }) + } } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 198d5b9874..f8ac89720e 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -5,9 +5,20 @@ import SwiftSignalKit import TelegramCore import Postbox +public struct ChatListNodePeersFilter: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let onlyWriteable = ChatListNodePeersFilter(rawValue: 1 << 0) + public static let onlyUsers = ChatListNodePeersFilter(rawValue: 1 << 1) +} + enum ChatListNodeMode { case chatList - case peers(onlyWriteable: Bool) + case peers(filter: ChatListNodePeersFilter) } struct ChatListNodeListViewTransition { @@ -44,10 +55,11 @@ final class ChatListNodeInteraction { let setPeerMuted: (PeerId, Bool) -> Void let deletePeer: (PeerId) -> Void let updatePeerGrouping: (PeerId, Bool) -> Void + let togglePeerMarkedUnread: (PeerId) -> Void var highlightedChatLocation: ChatListHighlightedLocation? - init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, messageSelected: @escaping (Message) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void) { + init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, messageSelected: @escaping (Message) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.messageSelected = messageSelected @@ -57,6 +69,7 @@ final class ChatListNodeInteraction { self.setPeerMuted = setPeerMuted self.deletePeer = deletePeer self.updatePeerGrouping = updatePeerGrouping + self.togglePeerMarkedUnread = togglePeerMarkedUnread } } @@ -114,20 +127,31 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities, isAd): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) - case let .peers(onlyWriteable): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true - if onlyWriteable { + if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { - enabled = canSendMessagesToPeer(peer) + if !canSendMessagesToPeer(peer) { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyUsers) { + if let peer = peer.peers[peer.peerId] { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } } else { enabled = false } @@ -153,20 +177,31 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities, isAd): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) - case let .peers(onlyWriteable): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true - if onlyWriteable { + if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { - enabled = canSendMessagesToPeer(peer) + if !canSendMessagesToPeer(peer) { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyUsers) { + if let peer = peer.peers[peer.peerId] { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } } else { enabled = false } @@ -197,6 +232,23 @@ private final class ChatListOpaqueTransactionState { } } +enum ChatListGlobalScrollOption { + case none + case top + case unread +} + +private struct ChatListVisibleUnreadCounts: Equatable { + var raw: Int32 = 0 + var filtered: Int32 = 0 +} + +enum ChatListNodeScrollPosition { + case auto + case autoUp + case top +} + final class ChatListNode: ListView { private let controlsHistoryPreload: Bool private let account: Account @@ -232,6 +284,29 @@ final class ChatListNode: ListView { private let chatListDisposable = MetaDisposable() private var activityStatusesDisposable: Disposable? + private let scrollToTopOptionPromise = Promise(.none) + var scrollToTopOption: Signal { + return self.scrollToTopOptionPromise.get() + } + + private let scrolledAtTop = ValuePromise(true) + private var scrolledAtTopValue: Bool = true { + didSet { + if self.scrolledAtTopValue != oldValue { + self.scrolledAtTop.set(self.scrolledAtTopValue) + } + } + } + + private let visibleUnreadCounts = ValuePromise(ChatListVisibleUnreadCounts()) + private var visibleUnreadCountsValue = ChatListVisibleUnreadCounts() { + didSet { + if self.visibleUnreadCountsValue != oldValue { + self.visibleUnreadCounts.set(self.visibleUnreadCountsValue) + } + } + } + init(account: Account, groupId: PeerGroupId?, controlsHistoryPreload: Bool, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { self.account = account self.controlsHistoryPreload = controlsHistoryPreload @@ -284,7 +359,8 @@ final class ChatListNode: ListView { } }) }, setPeerMuted: { [weak self] peerId, _ in - let _ = togglePeerMuted(account: account, peerId: peerId).start(completed: { + let _ = (togglePeerMuted(account: account, peerId: peerId) + |> deliverOnMainQueue).start(completed: { self?.updateState { return $0.withUpdatedPeerIdWithRevealedOptions(nil) } @@ -293,6 +369,17 @@ final class ChatListNode: ListView { self?.deletePeerChat?(peerId) }, updatePeerGrouping: { [weak self] peerId, group in self?.updatePeerGrouping?(peerId, group) + }, togglePeerMarkedUnread: { [weak self, weak account] peerId in + guard let account = account else { + return + } + + let _ = (togglePeerUnreadMarkInteractively(postbox: account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(completed: { + self?.updateState { + return $0.withUpdatedPeerIdWithRevealedOptions(nil) + } + }) }) let viewProcessingQueue = self.viewProcessingQueue @@ -306,7 +393,7 @@ final class ChatListNode: ListView { let previousView = Atomic(value: nil) let savedMessagesPeer: Signal - if case .peers(onlyWriteable: true) = mode { + if case let .peers(filter) = mode, filter == [.onlyWriteable] { savedMessagesPeer = account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) } else { savedMessagesPeer = .single(nil) @@ -369,20 +456,51 @@ final class ChatListNode: ListView { } self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in - if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.originalView { - var location: ChatListNodeLocation? - if range.firstIndex < 5 && view.laterIndex != nil { - location = .navigation(index: view.entries[view.entries.count - 1].index) - } else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil { - location = .navigation(index: view.entries[0].index) + if let strongSelf = self, let chatListView = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView { + let originalView = chatListView.originalView + if let range = range.loadedRange { + var location: ChatListNodeLocation? + if range.firstIndex < 5 && originalView.laterIndex != nil { + location = .navigation(index: originalView.entries[originalView.entries.count - 1].index) + } else if range.firstIndex >= 5 && range.lastIndex >= originalView.entries.count - 5 && originalView.earlierIndex != nil { + location = .navigation(index: originalView.entries[0].index) + } + + if let location = location, location != strongSelf.currentLocation { + strongSelf.currentLocation = location + strongSelf.chatListLocation.set(location) + } + + strongSelf.enqueueHistoryPreloadUpdate() } - if let location = location, location != strongSelf.currentLocation { - strongSelf.currentLocation = location - strongSelf.chatListLocation.set(location) + var rawUnreadCount: Int32 = 0 + var filteredUnreadCount: Int32 = 0 + if let range = range.visibleRange { + let entryCount = chatListView.filteredEntries.count + for i in range.firstIndex ..< range.lastIndex { + if i < 0 || i >= entryCount { + assertionFailure() + continue + } + switch chatListView.filteredEntries[entryCount - i - 1] { + case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _): + if let readState = readState { + let count = readState.count + rawUnreadCount += count + if let notificationSettings = notificationSettings, !notificationSettings.isRemovedFromTotalUnreadCount { + filteredUnreadCount += count + } + } + default: + break + } + } } - - strongSelf.enqueueHistoryPreloadUpdate() + var visibleUnreadCountsValue = strongSelf.visibleUnreadCountsValue + visibleUnreadCountsValue.raw = rawUnreadCount + visibleUnreadCountsValue.filtered = filteredUnreadCount + strongSelf.visibleUnreadCountsValue = visibleUnreadCountsValue } } @@ -418,14 +536,14 @@ final class ChatListNode: ListView { if foundAllPeers { return .single(cachedResult) } else { - return postbox.modify { modifier -> [PeerId: [(Peer, PeerInputActivity)]] in + return postbox.transaction { transaction -> [PeerId: [(Peer, PeerInputActivity)]] in var result: [PeerId: [(Peer, PeerInputActivity)]] = [:] var peerCache: [PeerId: Peer] = [:] for (chatPeerId, activities) in activitiesByPeerId { var chatResult: [(Peer, PeerInputActivity)] = [] for (peerId, activity) in activities { - if let peer = modifier.getPeer(peerId) { + if let peer = transaction.getPeer(peerId) { chatResult.append((peer, activity)) peerCache[peerId] = peer } @@ -505,8 +623,12 @@ final class ChatListNode: ListView { var referenceId: PinnedItemId? var beforeAll = false switch toEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): - referenceId = .peer(index.messageIndex.id.peerId) + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, isAd): + if isAd { + beforeAll = true + } else { + referenceId = .peer(index.messageIndex.id.peerId) + } case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): referenceId = .group(groupId) case .SearchEntry: @@ -516,12 +638,12 @@ final class ChatListNode: ListView { } if let _ = fromEntry.index.pinningIndex { - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - var itemIds = modifier.getPinnedItemIds() + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + var itemIds = transaction.getPinnedItemIds() var itemId: PinnedItemId? switch fromEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _): itemId = .peer(index.messageIndex.id.peerId) case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): itemId = .group(groupId) @@ -552,14 +674,53 @@ final class ChatListNode: ListView { } else { itemIds.append(itemId) } - reorderPinnedItemIds(modifier: modifier, itemIds: itemIds) - //modifier.setPinnedItemIds(itemIds) + reorderPinnedItemIds(transaction: transaction, itemIds: itemIds) + //transaction.setPinnedItemIds(itemIds) } }).start() } } } } + + self.scrollToTopOptionPromise.set(combineLatest( + renderedTotalUnreadCount(postbox: account.postbox) |> deliverOnMainQueue, + self.visibleUnreadCounts.get(), + self.scrolledAtTop.get() + ) |> map { badge, visibleUnreadCounts, scrolledAtTop -> ChatListGlobalScrollOption in + if scrolledAtTop { + if badge.0 != 0 { + switch badge.1 { + case .raw: + if visibleUnreadCounts.raw < badge.0 { + return .unread + } + case .filtered: + if visibleUnreadCounts.filtered < badge.0 { + return .unread + } + } + return .none + } else { + return .none + } + } else { + return .top + } + }) + + self.visibleContentOffsetChanged = { [weak self] offset in + if let strongSelf = self { + let atTop: Bool + switch offset { + case .none, .unknown: + atTop = false + case let .known(value): + atTop = value <= 0.0 + } + strongSelf.scrolledAtTopValue = atTop + } + } } deinit { @@ -645,6 +806,12 @@ final class ChatListNode: ListView { } } + if let scrollToItem = transition.scrollToItem, case .center = scrollToItem.position { + if let itemNode = strongSelf.itemNodeAtIndex(scrollToItem.index) as? ChatListItemNode { + itemNode.flashHighlight() + } + } + if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) @@ -672,9 +839,39 @@ final class ChatListNode: ListView { } } - func scrollToLatest() { - if let view = self.chatListView?.originalView, view.laterIndex == nil { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + func scrollToPosition(_ position: ChatListNodeScrollPosition) { + if let view = self.chatListView?.originalView { + if case .auto = position { + switch self.visibleContentOffset() { + case .none, .unknown: + if let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { + self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) + return + } + case let .known(offset): + if offset <= 0.0 { + self.scrollToEarliestUnread(earlierThan: nil) + return + } else { + if let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { + self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) + return + } + } + } + } else if case .autoUp = position, let maxVisibleChatListIndex = self.currentlyVisibleLatestChatListIndex() { + self.scrollToEarliestUnread(earlierThan: maxVisibleChatListIndex) + return + } + + if view.laterIndex == nil { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else { + let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound + , scrollPosition: .top(0.0), animated: true) + self.currentLocation = location + self.chatListLocation.set(location) + } } else { let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound , scrollPosition: .top(0.0), animated: true) @@ -683,6 +880,37 @@ final class ChatListNode: ListView { } } + func scrollToEarliestUnread(earlierThan: ChatListIndex?) { + let _ = (self.account.postbox.transaction { transaction -> ChatListIndex? in + var filter = true + if let inAppNotificationSettings = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings { + switch inAppNotificationSettings.totalUnreadCountDisplayStyle { + case .raw: + filter = false + case .filtered: + filter = true + } + } + return transaction.getEarliestUnreadChatListIndex(filtered: filter, earlierThan: earlierThan) + } |> deliverOnMainQueue).start(next: { [weak self] index in + guard let strongSelf = self else { + return + } + + if let index = index { + let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: self?.currentlyVisibleLatestChatListIndex() ?? ChatListIndex.absoluteUpperBound + , scrollPosition: .center(.top), animated: true) + strongSelf.currentLocation = location + strongSelf.chatListLocation.set(location) + } else { + let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound + , scrollPosition: .top(0.0), animated: true) + strongSelf.currentLocation = location + strongSelf.chatListLocation.set(location) + } + }) + } + private func enqueueHistoryPreloadUpdate() { } @@ -704,4 +932,26 @@ final class ChatListNode: ListView { } } } + + private func currentlyVisibleLatestChatListIndex() -> ChatListIndex? { + guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else { + return nil + } + if let range = self.displayedItemRange.visibleRange { + let entryCount = chatListView.filteredEntries.count + for i in range.firstIndex ..< range.lastIndex { + if i < 0 || i >= entryCount { + assertionFailure() + continue + } + switch chatListView.filteredEntries[entryCount - i - 1] { + case let .PeerEntry(index, _, _, readState, notificationSettings, _, _, _, _, _, _, _): + return index + default: + break + } + } + } + return nil + } } diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index ae482b5a9f..7b292d591c 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -53,7 +53,7 @@ enum ChatListNodeEntryId: Hashable { enum ChatListNodeEntry: Comparable, Identifiable { case SearchEntry(theme: PresentationTheme, text: String) - case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, inputActivities: [(Peer, PeerInputActivity)]?) + case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool) case HoleEntry(ChatListHole, theme: PresentationTheme) case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, message: Message?, topPeers: [Peer], counters: GroupReferenceUnreadCounters, editing: Bool) @@ -61,7 +61,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { switch self { case .SearchEntry: return ChatListIndex.absoluteUpperBound - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole, _): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) @@ -74,7 +74,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { switch self { case .SearchEntry: return .Search - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _): return .PeerId(index.messageIndex.id.peerId.toInt64()) case let .HoleEntry(hole, _): return .Hole(Int64(hole.index.id.id)) @@ -95,9 +95,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } - case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsInputActivities): + case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsInputActivities, lhsAd): switch rhs { - case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsInputActivities): + case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsInputActivities, rhsAd): if lhsIndex != rhsIndex { return false } @@ -151,6 +151,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else if (lhsInputActivities != nil) != (rhsInputActivities != nil) { return false } + if lhsAd != rhsAd { + return false + } return true default: @@ -203,15 +206,27 @@ enum ChatListNodeEntry: Comparable, Identifiable { } } +private func offsetPinnedIndex(_ index: ChatListIndex, offset: UInt16) -> ChatListIndex { + if let pinningIndex = index.pinningIndex { + return ChatListIndex(pinningIndex: pinningIndex + offset, messageIndex: index.messageIndex) + } else { + return index + } +} + func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, savedMessagesPeer: Peer?, mode: ChatListNodeMode) -> [ChatListNodeEntry] { var result: [ChatListNodeEntry] = [] + var pinnedIndexOffset: UInt16 = 0 + if view.laterIndex == nil && savedMessagesPeer == nil { + pinnedIndexOffset = UInt16(view.additionalItemEntries.count) + } loop: for entry in view.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo): if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { continue loop } - result.append(.PeerEntry(index: index, presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId])) + result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false)) case let .HoleEntry(hole): result.append(.HoleEntry(hole, theme: state.presentationData.theme)) case let .GroupReferenceEntry(groupId, index, message, topPeers, counters): @@ -222,7 +237,22 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, } if view.laterIndex == nil { if let savedMessagesPeer = savedMessagesPeer { - result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, inputActivities: nil)) + result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, inputActivities: nil, isAd: false)) + } else { + if !view.additionalItemEntries.isEmpty { + var pinningIndex: UInt16 = UInt16(view.additionalItemEntries.count - 1) + for entry in view.additionalItemEntries.reversed() { + switch entry { + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo): + result.append(.PeerEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: index.messageIndex), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: true)) + if pinningIndex != 0 { + pinningIndex -= 1 + } + default: + break + } + } + } } result.append(.SearchEntry(theme: state.presentationData.theme, text: view.groupId == nil ? state.presentationData.strings.DialogList_SearchLabel : "Search this feed")) } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 222b791081..39ec26daa1 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -94,7 +94,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - func item(account: Account, onlyWriteable: Bool, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { + func item(account: Account, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, account: account, peers: peers, peerSelected: { peer in @@ -114,13 +114,22 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } var enabled = true - if onlyWriteable { + if filter.contains(.onlyWriteable) { if let peer = chatPeer { enabled = canSendMessagesToPeer(peer) } else { enabled = false } } + if filter.contains(.onlyUsers) { + if let peer = chatPeer { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + } else { + enabled = false + } + } return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { clearRecentlySearchedPeers() @@ -246,7 +255,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - func item(account: Account, enableHeaders: Bool, onlyWriteable: Bool, interaction: ChatListNodeInteraction) -> ListViewItem { + func item(account: Account, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction) -> ListViewItem { switch self { case let .localPeer(peer, associatedPeer, _, theme, strings): let primaryPeer: Peer @@ -260,22 +269,36 @@ enum ChatListSearchEntry: Comparable, Identifiable { } var enabled = true - if onlyWriteable { + if filter.contains(.onlyWriteable) { if let peer = chatPeer { enabled = canSendMessagesToPeer(peer) } else { enabled = false } } + if filter.contains(.onlyUsers) { + if let peer = chatPeer { + if !(peer is TelegramUser || peer is TelegramSecretChat) { + enabled = false + } + } else { + enabled = false + } + } return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): var enabled = true - if onlyWriteable { + if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) } + if filter.contains(.onlyUsers) { + if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { + enabled = false + } + } var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { @@ -292,7 +315,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { interaction.peerSelected(peer.peer) }) case let .message(message, presentationData): - return ChatListItem(presentationData: presentationData, account: account, peerGroupId: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), content: .peer(message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil), editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, enableContextActions: false, interaction: interaction) + return ChatListItem(presentationData: presentationData, account: account, peerGroupId: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), content: .peer(message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, isAd: false), editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, enableContextActions: false, interaction: interaction) } } } @@ -310,22 +333,22 @@ struct ChatListSearchContainerTransition { let displayingResults: Bool } -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, onlyWriteable: Bool, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, onlyWriteable: onlyWriteable, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, onlyWriteable: onlyWriteable, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, filter: filter, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, filter: filter, peerSelected: peerSelected, peerLongTapped: peerLongTapped, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } -func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, enableHeaders: Bool, onlyWriteable: Bool, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition { +func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, onlyWriteable: onlyWriteable, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, onlyWriteable: onlyWriteable, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, filter: filter, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, enableHeaders: enableHeaders, filter: filter, interaction: interaction), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) } @@ -377,7 +400,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { return self._isSearching.get() } - init(account: Account, onlyWriteable: Bool, groupId: PeerGroupId?, openPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { + init(account: Account, filter: ChatListNodePeersFilter, groupId: PeerGroupId?, openPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { self.account = account self.openMessage = openMessage @@ -468,10 +491,12 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } - index = 0 - for message in foundRemoteMessages.0 { - entries.append(.message(message, presentationData)) - index += 1 + if !foundRemotePeers.2 { + index = 0 + for message in foundRemoteMessages.0 { + entries.append(.message(message, presentationData)) + index += 1 + } } return (entries, isSearching) @@ -508,6 +533,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in + }, togglePeerMarkedUnread: { _ in }) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) @@ -536,7 +562,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } let previousEntries = previousRecentItems.swap(entries) - let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, onlyWriteable: onlyWriteable, peerSelected: { peer in + let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, filter: filter, peerSelected: { peer in self?.recentListNode.clearHighlightAnimated(true) openPeer(peer) }, peerLongTapped: { peer in @@ -567,7 +593,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: account, enableHeaders: true, onlyWriteable: onlyWriteable, interaction: interaction) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: account, enableHeaders: true, filter: filter, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -750,7 +776,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { return (selectedItemNode.view, peer.id) } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { switch item.content { - case let .peer(message, peer, _, _, _, _, _): + case let .peer(message, peer, _, _, _, _, _, _): return (selectedItemNode.view, message?.id ?? peer.peerId) case let .groupReference(groupId, _, _, _): return (selectedItemNode.view, groupId) diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 791be445e7..4127687e2c 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -10,16 +10,25 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let controllerInteraction: ChatControllerInteraction private var multiplexedNode: MultiplexedVideoNode? + private let emptyNode: ImmediateTextNode private let disposable = MetaDisposable() private var validLayout: CGSize? - init(account: Account, controllerInteraction: ChatControllerInteraction) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction) { self.account = account self.controllerInteraction = controllerInteraction + self.emptyNode = ImmediateTextNode() + self.emptyNode.isLayerBacked = true + self.emptyNode.attributedText = NSAttributedString(string: strings.Conversation_EmptyGifPanelPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) + super.init() + + self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor + + self.addSubnode(self.emptyNode) } deinit { @@ -28,9 +37,13 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = size + let emptySize = self.emptyNode.updateLayout(size) + transition.updateFrame(node: self.emptyNode, frame: CGRect(origin: CGPoint(x: floor(size.width - emptySize.width) / 2.0, y: topInset + floor(size.height - topInset - emptySize.height) / 2.0), size: emptySize)) + if let multiplexedNode = self.multiplexedNode { + multiplexedNode.topInset = topInset multiplexedNode.bottomInset = bottomInset - let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) + let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame) multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition) } @@ -71,6 +84,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in if let strongSelf = self { strongSelf.multiplexedNode?.files = gifs + strongSelf.emptyNode.isHidden = !gifs.isEmpty } })) diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index b2a9d77e4e..bceeb1aec7 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -350,8 +350,12 @@ final class ChatMediaInputNode: ChatInputNode { }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) }) - self.gifPane = ChatMediaInputGifPane(account: account, controllerInteraction: controllerInteraction) - self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction) + self.gifPane = ChatMediaInputGifPane(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction) + + var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)? + self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction, getItemIsPreviewed: { item in + return getItemIsPreviewedImpl?(item) ?? false + }) self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers, .trending], currentIndex: 1, indexTransition: 0.0) @@ -389,7 +393,7 @@ final class ChatMediaInputNode: ChatInputNode { } }, openSettings: { [weak self] in if let strongSelf = self { - strongSelf.controllerInteraction.presentController(installedStickerPacksController(account: account, mode: .modal), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.controllerInteraction.navigationController()?.pushViewController(installedStickerPacksController(account: account, mode: .modal)) } }, toggleSearch: { [weak self] value in if let strongSelf = self { @@ -408,7 +412,14 @@ final class ChatMediaInputNode: ChatInputNode { } }) - self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor + getItemIsPreviewedImpl = { [weak self] item in + if let strongSelf = self { + return strongSelf.inputNodeInteraction.previewedStickerPackItem == .pack(item) + } + return false + } + + self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor self.collectionListPanel.addSubnode(self.listView) self.collectionListContainer.addSubnode(self.collectionListPanel) @@ -564,15 +575,15 @@ final class ChatMediaInputNode: ChatInputNode { if let stickerSearchContainerNode = strongSelf.stickerSearchContainerNode { panes = [] if let (itemNode, item) = stickerSearchContainerNode.itemAt(point: point.offsetBy(dx: -stickerSearchContainerNode.frame.minX, dy: -stickerSearchContainerNode.frame.minY)) { - return strongSelf.account.postbox.modify { modifier -> Bool in - return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + return strongSelf.account.postbox.transaction { transaction -> Bool in + return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [PeekControllerMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.controllerInteraction.sendSticker(item.file) } @@ -592,7 +603,7 @@ final class ChatMediaInputNode: ChatInputNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference) + let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController()) controller.sendSticker = { file in if let strongSelf = self { strongSelf.controllerInteraction.sendSticker(file) @@ -609,21 +620,21 @@ final class ChatMediaInputNode: ChatInputNode { }), PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: .found(item), menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: item, menu: menuItems)) } else { return nil } } } } else { - panes = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.stickerPane] + panes = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.trendingPane] } for pane in panes { if pane.supernode != nil, pane.frame.contains(point) { if let pane = pane as? ChatMediaInputGifPane { if let file = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(id: "", type: "gif", title: nil, description: nil, image: nil, file: file, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.controllerInteraction.sendGif(file) } @@ -635,17 +646,24 @@ final class ChatMediaInputNode: ChatInputNode { }) ]))) } - } else if let pane = pane as? ChatMediaInputStickerPane { - if let (itemNode, item) = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { - return strongSelf.account.postbox.modify { modifier -> Bool in - return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + } else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane { + var itemNodeAndItem: (ASDisplayNode, StickerPackItem)? + if let pane = pane as? ChatMediaInputStickerPane { + itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) + } else if let pane = pane as? ChatMediaInputTrendingPane { + itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) + } + + if let (itemNode, item) = itemNodeAndItem { + return strongSelf.account.postbox.transaction { transaction -> Bool in + return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [PeekControllerMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.controllerInteraction.sendSticker(item.file) } @@ -665,7 +683,7 @@ final class ChatMediaInputNode: ChatInputNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference) + let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController()) controller.sendSticker = { file in if let strongSelf = self { strongSelf.controllerInteraction.sendSticker(file) @@ -841,7 +859,7 @@ final class ChatMediaInputNode: ChatInputNode { panelHeight = standardInputHeight } - transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + self.collectionListPanelOffset + UIScreenPixel)))) + transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + /*self.collectionListPanelOffset + */UIScreenPixel)))) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) @@ -1076,6 +1094,7 @@ final class ChatMediaInputNode: ChatInputNode { } self.stickerSearchContainerNode?.updatePreviewing(animated: animated) + self.trendingPane.updatePreviewing(animated: animated) } } @@ -1166,4 +1185,15 @@ final class ChatMediaInputNode: ChatInputNode { } return super.hitTest(point, with: event) } + + static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets { + var insets = UIEdgeInsets() + if previousItem != nil { + insets.top += 3.0 + } + if nextItem != nil { + insets.bottom += 3.0 + } + return insets + } } diff --git a/TelegramUI/ChatMediaInputRecentGifsItem.swift b/TelegramUI/ChatMediaInputRecentGifsItem.swift index 35584970f1..f27aa07eb6 100644 --- a/TelegramUI/ChatMediaInputRecentGifsItem.swift +++ b/TelegramUI/ChatMediaInputRecentGifsItem.swift @@ -24,7 +24,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { async { let node = ChatMediaInputRecentGifsItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) node.updateIsHighlighted() @@ -36,7 +36,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { (node as? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme) }) } @@ -67,6 +67,8 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true + self.imageNode.contentMode = .center + self.imageNode.contentsScale = UIScreenScale self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) diff --git a/TelegramUI/ChatMediaInputSettingsItem.swift b/TelegramUI/ChatMediaInputSettingsItem.swift index 922732d3b0..adbb2a9d04 100644 --- a/TelegramUI/ChatMediaInputSettingsItem.swift +++ b/TelegramUI/ChatMediaInputSettingsItem.swift @@ -24,7 +24,7 @@ final class ChatMediaInputSettingsItem: ListViewItem { async { let node = ChatMediaInputSettingsItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) node.updateAppearanceTransition(transition: .immediate) @@ -35,7 +35,7 @@ final class ChatMediaInputSettingsItem: ListViewItem { } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { (node as? ChatMediaInputSettingsItemNode)?.updateTheme(theme: self.theme) }) } @@ -64,6 +64,8 @@ final class ChatMediaInputSettingsItemNode: ListViewItemNode { self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true + self.imageNode.contentMode = .center + self.imageNode.contentsScale = UIScreenScale self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize) diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index f7344deae0..5447c0464a 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -159,7 +159,8 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { super.layout() let bounds = self.bounds - let boundingSize = bounds.insetBy(dx: 6.0, dy: 6.0).size + let sideSize: CGFloat = min(75.0 - 6.0, bounds.width) + let boundingSize = CGSize(width: sideSize, height: sideSize) if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index d3f5ba90f0..e3f503c403 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -31,8 +31,8 @@ final class ChatMediaInputStickerPackItem: ListViewItem { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputStickerPackItemNode() - node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.contentSize = boundingSize + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction completion(node, { return (nil, { @@ -44,7 +44,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { (node as? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) }) } @@ -55,7 +55,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { } private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 30.0, height: 30.0) +private let boundingImageSize = CGSize(width: 28.0, height: 28.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index ae21d6a66e..81d8d63e63 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -22,7 +22,7 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { self.addSubnode(self.gridNode) self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in - if let strongSelf = self { + if let strongSelf = self, !transition.isAnimated { let offset = -(layout.contentOffset.y + 41.0) var relativeChange: CGFloat = 0.0 if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset { @@ -42,7 +42,11 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { } override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + let sideInset: CGFloat = 2.0 + var itemSide: CGFloat = floor((size.width - sideInset * 2.0) / 5.0) + itemSide = min(itemSide, 75.0) + let itemSize = CGSize(width: itemSide, height: itemSide) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), preloadSize: 300.0, type: .fixed(itemSize: itemSize, lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) } diff --git a/TelegramUI/ChatMediaInputTrendingItem.swift b/TelegramUI/ChatMediaInputTrendingItem.swift index 64591dba38..83274cffbe 100644 --- a/TelegramUI/ChatMediaInputTrendingItem.swift +++ b/TelegramUI/ChatMediaInputTrendingItem.swift @@ -24,7 +24,7 @@ final class ChatMediaInputTrendingItem: ListViewItem { async { let node = ChatMediaInputTrendingItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction node.updateTheme(theme: self.theme) node.updateIsHighlighted() @@ -36,7 +36,7 @@ final class ChatMediaInputTrendingItem: ListViewItem { } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { (node as? ChatMediaInputTrendingItemNode)?.updateTheme(theme: self.theme) }) } @@ -67,6 +67,8 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true + self.imageNode.contentMode = .center + self.imageNode.contentsScale = UIScreenScale self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index e96f0397bc..fd56d63b1c 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -8,10 +8,12 @@ import SwiftSignalKit final class TrendingPaneInteraction { let installPack: (ItemCollectionInfo) -> Void let openPack: (ItemCollectionInfo) -> Void + let getItemIsPreviewed: (StickerPackItem) -> Bool - init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void) { + init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.installPack = installPack self.openPack = openPack + self.getItemIsPreviewed = getItemIsPreviewed } } @@ -91,6 +93,7 @@ private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], ins final class ChatMediaInputTrendingPane: ChatMediaInputPane { private let account: Account private let controllerInteraction: ChatControllerInteraction + private let getItemIsPreviewed: (StickerPackItem) -> Bool private let listNode: ListView @@ -100,9 +103,10 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { private var disposable: Disposable? private var isActivated = false - init(account: Account, controllerInteraction: ChatControllerInteraction) { + init(account: Account, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.account = account self.controllerInteraction = controllerInteraction + self.getItemIsPreviewed = getItemIsPreviewed self.listNode = ListView() @@ -145,30 +149,36 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { }, openPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { strongSelf.view.window?.endEditing(true) - strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()) + controller.sendSticker = { file in + if let strongSelf = self { + strongSelf.controllerInteraction.sendSticker(file) + } + } + strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } - }) + }, getItemIsPreviewed: self.getItemIsPreviewed) let previousEntries = Atomic<[TrendingPaneEntry]?>(value: nil) let account = self.account self.disposable = (combineLatest(account.viewTracker.featuredStickerPacks(), account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) - |> map { trendingEntries, view -> TrendingPaneTransition in - var installedPacks = Set() - if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { - if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { - for entry in packsEntries { - installedPacks.insert(entry.id) - } + |> map { trendingEntries, view -> TrendingPaneTransition in + var installedPacks = Set() + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + for entry in packsEntries { + installedPacks.insert(entry.id) } } - let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks) - let previous = previousEntries.swap(entries) - - return preparedTransition(from: previous ?? [], to: entries, account: account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, initial: previous == nil) } - |> deliverOnMainQueue).start(next: { [weak self] transition in - self?.enqueueTransition(transition) - }) + let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks) + let previous = previousEntries.swap(entries) + + return preparedTransition(from: previous ?? [], to: entries, account: account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, initial: previous == nil) + } + |> deliverOnMainQueue).start(next: { [weak self] transition in + self?.enqueueTransition(transition) + }) } override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -230,8 +240,31 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { //options.insert(.AnimateCrossfade) } - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } } + + func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + let localPoint = self.view.convert(point, to: self.listNode.view) + var resultNode: MediaInputPaneTrendingItemNode? + self.listNode.forEachItemNode { itemNode in + if itemNode.frame.contains(localPoint), let itemNode = itemNode as? MediaInputPaneTrendingItemNode { + resultNode = itemNode + } + } + if let resultNode = resultNode { + return resultNode.itemAt(point: self.listNode.view.convert(localPoint, to: resultNode.view)) + } + + return nil + } + + func updatePreviewing(animated: Bool) { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? MediaInputPaneTrendingItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + } } diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 3d1b2ca08a..9464e25066 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -370,12 +370,18 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P typesString.append("personal detail") case .passport: typesString.append("passport") + case .internalPassport: + typesString.append("internal passport") case .driversLicense: typesString.append("passport") case .idCard: typesString.append("ID card") case .address: typesString.append("residential address") + case .passportRegistration: + typesString.append("passport registration") + case .temporaryRegistration: + typesString.append("temporary registration") case .bankStatement: typesString.append("bank statement") case .utilityBill: diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index b466f62472..04b93d4288 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -108,6 +108,11 @@ private let chatMessagePeerIdColors: [UIColor] = [ UIColor(rgb: 0x3d72ed) ] +private enum ContentNodeOperation { + case remove(index: Int) + case insert(index: Int, node: ChatMessageBubbleContentNode) +} + class ChatMessageBubbleItemNode: ChatMessageItemView { private let backgroundNode: ChatMessageBackground private var transitionClippingNode: ASDisplayNode? @@ -117,6 +122,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var swipeToReplyFeedback: HapticFeedback? private var nameNode: TextNode? + private var adminBadgeNode: TextNode? private var forwardInfoNode: ChatMessageForwardInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? @@ -179,6 +185,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.nameNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.adminBadgeNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.forwardInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.replyInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -269,6 +276,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } let authorNameLayout = TextNode.asyncLayout(self.nameNode) + let adminBadgeLayout = TextNode.asyncLayout(self.adminBadgeNode) let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) @@ -399,7 +407,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) var contentPropertiesAndPrepareLayouts: [(Message, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] - var addedContentNodes: [ChatMessageBubbleContentNode]? + var addedContentNodes: [(Message, ChatMessageBubbleContentNode)]? let contentNodeMessagesAndClasses = contentNodeMessagesAndClassesForItem(item) for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses { @@ -417,11 +425,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if addedContentNodes == nil { addedContentNodes = [] } - addedContentNodes!.append(contentNode) + addedContentNodes!.append((contentNodeMessage, contentNode)) } } var authorNameString: String? + let authorIsAdmin: Bool + switch content { + case let .message(_, _, _, isAdmin): + authorIsAdmin = isAdmin + case .group: + authorIsAdmin = false + } var inlineBotNameString: String? var replyMessage: Message? var replyMarkup: ReplyMarkupMessageAttribute? @@ -459,7 +474,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let read: Bool switch item.content { - case let .message(_, value, _): + case let .message(_, value, _, _): read = value case let .group(messages): read = messages[0].1 @@ -514,7 +529,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .message: break case let .group(messages): - for (m, _, selection) in messages { + for (m, _, selection, _) in messages { if m.id == message.id { switch selection { case .none: @@ -672,6 +687,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var nameNodeOriginY: CGFloat = 0.0 var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) + var adminNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) var replyInfoOriginY: CGFloat = 0.0 var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil }) @@ -688,6 +704,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let inlineBotNameColor = incoming ? item.presentationData.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.chat.bubble.outgoingAccentTextColor let attributedString: NSAttributedString + var adminBadgeString: NSAttributedString? + if authorIsAdmin { + adminBadgeString = NSAttributedString(string: " \(item.presentationData.strings.Conversation_Admin)", font: inlineBotPrefixFont, textColor: incoming ? item.presentationData.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.chat.bubble.outgoingSecondaryTextColor) + } if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString { let mutableString = NSMutableAttributedString(string: "\(authorNameString) ", attributes: [NSAttributedStringKey.font: nameFont, NSAttributedStringKey.foregroundColor: authorNameColor]) @@ -706,12 +726,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { attributedString = NSAttributedString(string: "", font: nameFont, textColor: inlineBotNameColor) } - let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let adminBadgeSizeAndApply = adminBadgeLayout(TextNodeLayoutArguments(attributedString: adminBadgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + adminNodeSizeApply = (adminBadgeSizeAndApply.0.size, { + return adminBadgeSizeAndApply.1() + }) + + let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - adminBadgeSizeAndApply.0.size.width), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) nameNodeSizeApply = (sizeAndApply.0.size, { return sizeAndApply.1() }) + nameNodeOriginY = headerSize.height - headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) + headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + adminBadgeSizeAndApply.0.size.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) headerSize.height += nameNodeSizeApply.0.height } @@ -994,14 +1020,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let backgroundFrame: CGRect let contentOrigin: CGPoint + let contentUpperRightCorner: CGPoint switch alignment { case .none: backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) + contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) case .center: let availableWidth = params.width - params.leftInset - params.rightInset backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize) contentOrigin = CGPoint(x: backgroundFrame.minX + floor(layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) / 2.0, y: backgroundFrame.minY + layoutConstants.bubble.contentInsets.top + headerSize.height) + contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) } var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) @@ -1092,9 +1121,30 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.addSubnode(nameNode) } nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0) + + if let adminBadgeNode = adminNodeSizeApply.1() { + strongSelf.adminBadgeNode = adminBadgeNode + let adminBadgeFrame = CGRect(origin: CGPoint(x: contentUpperRightCorner.x - layoutConstants.text.bubbleInsets.left - adminNodeSizeApply.0.width, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: adminNodeSizeApply.0) + if adminBadgeNode.supernode == nil { + if !adminBadgeNode.isNodeLoaded { + adminBadgeNode.isLayerBacked = true + } + strongSelf.addSubnode(adminBadgeNode) + adminBadgeNode.frame = adminBadgeFrame + } else { + let previousAdminBadgeFrame = adminBadgeNode.frame + adminBadgeNode.frame = adminBadgeFrame + transition.animatePositionAdditive(node: adminBadgeNode, offset: CGPoint(x: previousAdminBadgeFrame.maxX - adminBadgeFrame.maxX, y: 0.0)) + } + } else { + strongSelf.adminBadgeNode?.removeFromSupernode() + strongSelf.adminBadgeNode = nil + } } else { strongSelf.nameNode?.removeFromSupernode() strongSelf.nameNode = nil + strongSelf.adminBadgeNode?.removeFromSupernode() + strongSelf.adminBadgeNode = nil } if let forwardInfoNode = forwardInfoSizeApply.1() { @@ -1153,7 +1203,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if let addedContentNodes = addedContentNodes { - for contentNode in addedContentNodes { + for (contentNodeMessage, contentNode) in addedContentNodes { updatedContentNodes.append(contentNode) strongSelf.addSubnode(contentNode) @@ -1161,7 +1211,27 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - strongSelf.contentNodes = updatedContentNodes + var sortedContentNodes: [ChatMessageBubbleContentNode] = [] + outer: for (message, nodeClass) in contentNodeMessagesAndClasses { + if let addedContentNodes = addedContentNodes { + for (contentNodeMessage, contentNode) in addedContentNodes { + if type(of: contentNode) == nodeClass && contentNodeMessage.stableId == message.stableId { + sortedContentNodes.append(contentNode) + continue outer + } + } + } + for contentNode in updatedContentNodes { + if type(of: contentNode) == nodeClass && contentNode.item?.message.stableId == message.stableId { + sortedContentNodes.append(contentNode) + continue outer + } + } + } + + assert(sortedContentNodes.count == updatedContentNodes.count) + + strongSelf.contentNodes = sortedContentNodes } var contentNodeIndex = 0 @@ -1177,7 +1247,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var animateFrame = false var animateAlpha = false if let addedContentNodes = addedContentNodes { - if !addedContentNodes.contains(where: { $0 === contentNode }) { + if !addedContentNodes.contains(where: { $0.1 === contentNode }) { animateFrame = true } else { animateAlpha = true @@ -1631,11 +1701,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var incoming = true switch item.content { - case let .message(message, _, _): + case let .message(message, _, _, _): selected = selectionState.selectedIds.contains(message.id) case let .group(messages: messages): var allSelected = !messages.isEmpty - for (message, _, _) in messages { + for (message, _, _, _) in messages { if !selectionState.selectedIds.contains(message.id) { allSelected = false break @@ -1656,7 +1726,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { switch item.content { - case let .message(message, _, _): + case let .message(message, _, _, _): item.controllerInteraction.toggleMessagesSelection([message.id], value) case let .group(messages): item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) diff --git a/TelegramUI/ChatMessageForwardInfoNode.swift b/TelegramUI/ChatMessageForwardInfoNode.swift index 4172a7f8f7..37512ff979 100644 --- a/TelegramUI/ChatMessageForwardInfoNode.swift +++ b/TelegramUI/ChatMessageForwardInfoNode.swift @@ -24,9 +24,9 @@ class ChatMessageForwardInfoNode: ASDisplayNode { return { theme, strings, type, peer, authorName, constrainedSize in let peerString: String if let authorName = authorName { - peerString = "\(peer.displayTitle(or: strings.Peer_DeletedUser)) (\(authorName))" + peerString = "\(peer.displayTitle(strings: strings)) (\(authorName))" } else { - peerString = peer.displayTitle(or: strings.Peer_DeletedUser) + peerString = peer.displayTitle(strings: strings) } let titleColor: UIColor diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 3fd97e1f7b..da0084ed2a 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -696,8 +696,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { case .Fetching: if item.message.flags.isSending { let messageId = item.message.id - let _ = item.account.postbox.modify({ modifier -> Void in - modifier.deleteMessages([messageId]) + let _ = item.account.postbox.transaction({ transaction -> Void in + transaction.deleteMessages([messageId]) }).start() } else { self.videoNode?.fetchControl(.cancel) diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 5c1c7afcb8..59dbcdbd10 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -82,8 +82,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { switch resourceStatus { case let .fetchStatus(fetchStatus): if let account = self.account, let message = self.message, message.flags.isSending { - let _ = account.postbox.modify({ modifier -> Void in - modifier.deleteMessages([message.id]) + let _ = account.postbox.transaction({ transaction -> Void in + transaction.deleteMessages([message.id]) }).start() } else { switch fetchStatus { diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 1839e8a0b2..f3a7f81a92 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -79,8 +79,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { switch fetchStatus { case .Fetching: if let account = self.account, let (messageId, flags) = self.messageIdAndFlags, flags.isSending { - let _ = account.postbox.modify({ modifier -> Void in - modifier.deleteMessages([messageId]) + let _ = account.postbox.transaction({ transaction -> Void in + transaction.deleteMessages([messageId]) }).start() } if let cancel = self.fetchControls.with({ return $0?.cancel }) { diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 07f931ed45..3455c9f7e7 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -7,12 +7,12 @@ import SwiftSignalKit import TelegramCore public enum ChatMessageItemContent: Sequence { - case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection) - case group(messages: [(Message, Bool, ChatHistoryMessageSelection)]) + case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, isAdmin: Bool) + case group(messages: [(Message, Bool, ChatHistoryMessageSelection, Bool)]) func effectivelyIncoming(_ accountPeerId: PeerId) -> Bool { switch self { - case let .message(message, _, _): + case let .message(message, _, _, _): return message.effectivelyIncoming(accountPeerId) case let .group(messages): return messages[0].0.effectivelyIncoming(accountPeerId) @@ -21,7 +21,7 @@ public enum ChatMessageItemContent: Sequence { var index: MessageIndex { switch self { - case let .message(message, _, _): + case let .message(message, _, _, _): return MessageIndex(message) case let .group(messages): return MessageIndex(messages[0].0) @@ -30,7 +30,7 @@ public enum ChatMessageItemContent: Sequence { var firstMessage: Message { switch self { - case let .message(message, _, _): + case let .message(message, _, _, _): return message case let .group(messages): return messages[0].0 @@ -41,7 +41,7 @@ public enum ChatMessageItemContent: Sequence { var index = 0 return AnyIterator { () -> Message? in switch self { - case let .message(message, _, _): + case let .message(message, _, _, _): if index == 0 { index += 1 return message @@ -202,7 +202,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { var message: Message { switch self.content { - case let .message(message, _, _): + case let .message(message, _, _, _): return message case let .group(messages): return messages[0].0 @@ -211,7 +211,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { var read: Bool { switch self.content { - case let .message(_, read, _): + case let .message(_, read, _, _): return read case let .group(messages): return messages[0].1 diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 1e1b03300b..ee9f1cbbc3 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -164,12 +164,12 @@ public class ChatMessageItemView: ListViewItemNode { var isHighlightedInOverlay = false if let item = self.item, let contextHighlightedState = item.controllerInteraction.contextHighlightedState { switch item.content { - case let .message(message, _, _): + case let .message(message, _, _, _): if contextHighlightedState.messageStableId == message.stableId { isHighlightedInOverlay = true } case let .group(messages): - for (message, _, _) in messages { + for (message, _, _, _) in messages { if contextHighlightedState.messageStableId == message.stableId { isHighlightedInOverlay = true break diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index 659829f463..8aa267f283 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -131,6 +131,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } + if item.message.containsSecretMedia { + imageDimensions = nil + } + let imageNodeLayout = self.imageNode.asyncLayout() var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 665bf91a6a..392a0372f0 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -86,7 +86,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let displaySize = CGSize(width: 200.0, height: 200.0) + let displaySize = CGSize(width: 162.0, height: 162.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants let imageLayout = self.imageNode.asyncLayout() @@ -206,7 +206,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { imageApply() dateAndStatusApply(false) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { if strongSelf.replyBackgroundNode == nil { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index c5b11c8857..4e8c9eaa08 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -11,13 +11,13 @@ public enum ChatFinishMediaRecordingAction { } final class ChatPanelInterfaceInteractionStatuses { - let editingMessage: Signal + let editingMessage: Signal let startingBot: Signal let unblockingPeer: Signal let searching: Signal let loadingMessage: Signal - init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal, searching: Signal, loadingMessage: Signal) { + init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal, searching: Signal, loadingMessage: Signal) { self.editingMessage = editingMessage self.startingBot = startingBot self.unblockingPeer = unblockingPeer @@ -31,9 +31,19 @@ enum ChatPanelSearchNavigationAction { case later } +enum ChatPanelRestrictionInfoSubject { + case mediaRecording + case stickers +} + +struct SetupEditMessageMediaResult { + let f: (Media) -> Void +} + final class ChatPanelInterfaceInteraction { let setupReplyMessage: (MessageId) -> Void - let setupEditMessage: (MessageId) -> Void + let setupEditMessage: (MessageId?) -> Void + let setupEditMessageMedia: () -> Void let beginMessageSelection: ([MessageId]) -> Void let deleteSelectedMessages: () -> Void let deleteMessages: ([Message]) -> Void @@ -62,6 +72,7 @@ final class ChatPanelInterfaceInteraction { let lockMediaRecording: () -> Void let deleteRecordedMedia: () -> Void let sendRecordedMedia: () -> Void + let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject) -> Void let switchMediaRecordingMode: () -> Void let setupMessageAutoremoveTimeout: () -> Void let sendSticker: (TelegramMediaFile) -> Void @@ -80,9 +91,10 @@ final class ChatPanelInterfaceInteraction { let toggleSilentPost: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, setupEditMessageMedia: @escaping () -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage + self.setupEditMessageMedia = setupEditMessageMedia self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.deleteMessages = deleteMessages @@ -111,6 +123,7 @@ final class ChatPanelInterfaceInteraction { self.lockMediaRecording = lockMediaRecording self.deleteRecordedMedia = deleteRecordedMedia self.sendRecordedMedia = sendRecordedMedia + self.displayRestrictedInfo = displayRestrictedInfo self.switchMediaRecordingMode = switchMediaRecordingMode self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout self.sendSticker = sendSticker diff --git a/TelegramUI/ChatPresentationData.swift b/TelegramUI/ChatPresentationData.swift index 4f86c66f11..2a063136ef 100644 --- a/TelegramUI/ChatPresentationData.swift +++ b/TelegramUI/ChatPresentationData.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import TelegramCore extension PresentationFontSize { var baseDisplaySize: CGFloat { diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index efa2e29840..16fc521941 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -8,7 +8,7 @@ enum ChatPresentationInputQueryKind: Int32 { case mention case command case contextRequest - case stickerSearch + case emojiSearch } struct ChatInputQueryMentionTypes: OptionSet { @@ -28,7 +28,7 @@ enum ChatPresentationInputQuery: Hashable, Equatable { case hashtag(String) case mention(query: String, types: ChatInputQueryMentionTypes) case command(String) - case stickerSearch(String) + case emojiSearch(String) case contextRequest(addressName: String, query: String) var kind: ChatPresentationInputQueryKind { @@ -43,8 +43,8 @@ enum ChatPresentationInputQuery: Hashable, Equatable { return .command case .contextRequest: return .contextRequest - case .stickerSearch: - return .stickerSearch + case .emojiSearch: + return .emojiSearch } } @@ -59,52 +59,11 @@ enum ChatPresentationInputQuery: Hashable, Equatable { case let .command(value): return 4 &+ value.hashValue case let .contextRequest(addressName, query): - return 5 &+ addressName.hashValue &* 31 + query.hashValue - case let .stickerSearch(value): + return 5 &+ addressName.hashValue &* 31 &+ query.hashValue + case let .emojiSearch(value): return 6 &+ value.hashValue } } - - static func ==(lhs: ChatPresentationInputQuery, rhs: ChatPresentationInputQuery) -> Bool { - switch lhs { - case let .emoji(query): - if case .emoji(query) = rhs { - return true - } else { - return false - } - case let .hashtag(query): - if case .hashtag(query) = rhs { - return true - } else { - return false - } - case let .mention(query, types): - if case .mention(query, types) = rhs { - return true - } else { - return false - } - case let .command(query): - if case .command(query) = rhs { - return true - } else { - return false - } - case let .stickerSearch(value): - if case .stickerSearch(value) = rhs { - return true - } else { - return false - } - case let .contextRequest(addressName, query): - if case .contextRequest(addressName, query) = rhs { - return true - } else { - return false - } - } - } } enum ChatPresentationInputQueryResult: Equatable { @@ -405,6 +364,7 @@ struct ChatPresentationInterfaceState: Equatable { let chatLocation: ChatLocation let renderedPeer: RenderedPeer? let inputTextPanelState: ChatTextInputPanelState + let editMessageState: ChatEditInterfaceMessageState? let recordedMediaPreview: ChatRecordedMediaPreview? let inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] let inputMode: ChatInputMode @@ -431,6 +391,7 @@ struct ChatPresentationInterfaceState: Equatable { init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() + self.editMessageState = nil self.recordedMediaPreview = nil self.chatLocation = chatLocation self.renderedPeer = nil @@ -457,11 +418,12 @@ struct ChatPresentationInterfaceState: Equatable { self.mode = mode } - init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, inputTextPanelState: ChatTextInputPanelState, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode) { + init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer self.inputTextPanelState = inputTextPanelState + self.editMessageState = editMessageState self.recordedMediaPreview = recordedMediaPreview self.inputQueryResults = inputQueryResults self.inputMode = inputMode @@ -498,6 +460,10 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.editMessageState != rhs.editMessageState { + return false + } + if lhs.recordedMediaPreview != rhs.recordedMediaPreview { return false } @@ -618,11 +584,11 @@ struct ChatPresentationInterfaceState: Equatable { } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -633,75 +599,79 @@ struct ChatPresentationInterfaceState: Equatable { } else { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: f(self.inputTextPanelState), recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + } + + func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedRecordedMediaPreview(_ recordedMediaPreview: ChatRecordedMediaPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessage(_ pinnedMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode) } } diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift index 183a195098..3ac1e0109d 100644 --- a/TelegramUI/ChatRecentActionsController.swift +++ b/TelegramUI/ChatRecentActionsController.swift @@ -38,69 +38,13 @@ final class ChatRecentActionsController: ViewController { self.panelInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in + }, setupEditMessageMedia: { }, beginMessageSelection: { _ in }, deleteSelectedMessages: { }, deleteMessages: { _ in - }, forwardSelectedMessages: { [weak self] in - /*if let strongSelf = self { - if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { - let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - - let controller = PeerSelectionController(account: strongSelf.account) - controller.peerSelected = { [weak controller] peerId in - if let strongSelf = self, let _ = controller { - let _ = (strongSelf.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in - if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedForwardMessageIds(forwardMessageIds) - } else { - return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) - } - }) - }) |> deliverOnMainQueue).start(completed: { - if let strongSelf = self { - strongSelf.updateInterfaceState(animated: false, { $0.withoutSelectionState() }) - - let ready = ValuePromise() - - strongSelf.messageContextDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in - if let strongController = controller { - strongController.dismiss() - } - })) - - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) - } - }) - } - } - strongSelf.present(controller, in: .window(.root)) - } - }*/ + }, forwardSelectedMessages: { }, forwardMessages: { _ in - }, shareSelectedMessages: { [weak self] in - /*if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { - let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in - var messages: [Message] = [] - for id in selectedIds { - if let message = modifier.getMessage(id) { - messages.append(message) - } - } - return messages - } |> deliverOnMainQueue).start(next: { messages in - if let strongSelf = self, !messages.isEmpty { - strongSelf.updateInterfaceState(animated: true, { - $0.withoutSelectionState() - }) - - let shareController = ShareController(account: strongSelf.account, subject: .messages(messages.sorted(by: { lhs, rhs in - return MessageIndex(lhs) < MessageIndex(rhs) - })), externalShare: true, immediateExternalShare: true) - strongSelf.present(shareController, in: .window(.root)) - } - }) - }*/ + }, shareSelectedMessages: { }, updateTextInputState: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { @@ -123,6 +67,7 @@ final class ChatRecentActionsController: ViewController { }, lockMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { + }, displayRestrictedInfo: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 2017663d2a..5546545e4c 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -208,7 +208,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } })) }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in + }, presentController: { _, _ in + }, navigationController: { [weak self] in + return self?.getNavigationController() + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in if let strongSelf = self { switch action { case let .url(url): @@ -642,7 +645,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId) } case let .stickerPack(name): - strongSelf.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name)), nil) + strongSelf.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name), parentNavigationController: strongSelf.getNavigationController()), nil) case let .instantView(webpage, anchor): strongSelf.pushController(InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor)) case let .join(link): diff --git a/TelegramUI/ChatRecentActionsHistoryTransition.swift b/TelegramUI/ChatRecentActionsHistoryTransition.swift index 34c73e543a..97903921b4 100644 --- a/TelegramUI/ChatRecentActionsHistoryTransition.swift +++ b/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -104,7 +104,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -135,14 +135,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case .content: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -173,7 +173,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -192,7 +192,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -210,7 +210,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -237,7 +237,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .toggleSignatures(value): var peers = SimpleDictionary() var author: Peer? @@ -264,7 +264,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .updatePinned(message): switch self.id.contentIndex { case .header: @@ -286,7 +286,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case .content: if let message = message { var peers = SimpleDictionary() @@ -304,7 +304,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) } else { var peers = SimpleDictionary() var author: Peer? @@ -326,7 +326,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) } } case let .editMessage(prev, message): @@ -352,7 +352,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -369,7 +369,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, isAdmin: false), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -395,7 +395,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -412,7 +412,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -430,7 +430,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -446,7 +446,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -547,7 +547,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -616,7 +616,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -645,7 +645,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) case let .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -657,7 +657,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - if value { + if !value { appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageGroupPreHistoryVisible(author?.displayTitle ?? ""), generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] @@ -675,7 +675,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) } } } diff --git a/TelegramUI/ChatRestrictedInputPanelNode.swift b/TelegramUI/ChatRestrictedInputPanelNode.swift new file mode 100644 index 0000000000..eea908800b --- /dev/null +++ b/TelegramUI/ChatRestrictedInputPanelNode.swift @@ -0,0 +1,47 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class ChatRestrictedInputPanelNode: ChatInputPanelNode { + private let textNode: ImmediateTextNode + + private var presentationInterfaceState: ChatPresentationInterfaceState? + + override init() { + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 2 + self.textNode.textAlignment = .center + + super.init() + + self.addSubnode(self.textNode) + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + } + + if let renderedPeer = interfaceState.renderedPeer, let channel = renderedPeer.peer as? TelegramChannel, let bannedRights = channel.bannedRights { + if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedTextTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: interfaceState.strings, timeFormat: .regular)).0, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } else { + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } + } + + let panelHeight: CGFloat = 47.0 + let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) + + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0), y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) + + return panelHeight + } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + return 47.0 + } +} diff --git a/TelegramUI/ChatTextInputAccessoryItem.swift b/TelegramUI/ChatTextInputAccessoryItem.swift index 8563b39f21..baf7a8546c 100644 --- a/TelegramUI/ChatTextInputAccessoryItem.swift +++ b/TelegramUI/ChatTextInputAccessoryItem.swift @@ -2,50 +2,9 @@ import Foundation enum ChatTextInputAccessoryItem: Equatable { case keyboard - case stickers + case stickers(Bool) case inputButtons case commands case silentPost(Bool) case messageAutoremoveTimeout(Int32?) - - static func ==(lhs: ChatTextInputAccessoryItem, rhs: ChatTextInputAccessoryItem) -> Bool { - switch lhs { - case .keyboard: - if case .keyboard = rhs { - return true - } else { - return false - } - case .stickers: - if case .stickers = rhs { - return true - } else { - return false - } - case .inputButtons: - if case .inputButtons = rhs { - return true - } else { - return false - } - case .commands: - if case .commands = rhs { - return true - } else { - return false - } - case let .silentPost(value): - if case .silentPost(value) = rhs { - return true - } else { - return false - } - case let .messageAutoremoveTimeout(lhsTimeout): - if case let .messageAutoremoveTimeout(rhsTimeout) = rhs, lhsTimeout == rhsTimeout { - return true - } else { - return false - } - } - } } diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift index 7066f1b331..426571f6c4 100644 --- a/TelegramUI/ChatTextInputMediaRecordingButton.swift +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -156,6 +156,7 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto var mode: ChatTextInputMediaRecordingButtonMode = .audio var account: Account? let presentController: (ViewController) -> Void + var recordingDisabled: () -> Void = { } var beginRecording: () -> Void = { } var endRecording: (Bool) -> Void = { _ in } var stopRecording: () -> Void = { } @@ -324,15 +325,19 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto } func micButtonInteractionBegan() { - self.modeTimeoutTimer?.invalidate() - let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in - if let strongSelf = self { - strongSelf.modeTimeoutTimer = nil - strongSelf.beginRecording() - } - }, queue: Queue.mainQueue()) - self.modeTimeoutTimer = modeTimeoutTimer - modeTimeoutTimer.start() + if self.fadeDisabled { + self.recordingDisabled() + } else { + self.modeTimeoutTimer?.invalidate() + let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.modeTimeoutTimer = nil + strongSelf.beginRecording() + } + }, queue: Queue.mainQueue()) + self.modeTimeoutTimer = modeTimeoutTimer + modeTimeoutTimer.start() + } } func micButtonInteractionCancelled(_ velocity: CGPoint) { diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 2a1ad740fe..fe65d7e8ee 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -29,8 +29,9 @@ private final class AccessoryItemIconButton: HighlightableButton { switch item { case .keyboard: self.setImage(PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), for: []) - case .stickers: + case let .stickers(enabled): self.setImage(PresentationResourcesChat.chatInputTextFieldStickersImage(theme), for: []) + self.imageView?.alpha = enabled ? 1.0 : 0.5 case .inputButtons: self.setImage(PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), for: []) case .commands: @@ -101,13 +102,13 @@ private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPres let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) let result: CGFloat if baseFontSize.isEqual(to: 17.0) { - result = 33.0 + result = 31.0 } else if baseFontSize.isEqual(to: 19.0) { - result = 35.0 - } else if baseFontSize.isEqual(to: 21.0) { - result = 38.0 - } else { result = 33.0 + } else if baseFontSize.isEqual(to: 21.0) { + result = 35.0 + } else { + result = 31.0 } return result } @@ -129,7 +130,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor, strokeColor: UIC context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) context.setBlendMode(.normal) context.setStrokeColor(strokeColor.cgColor) - let strokeWidth: CGFloat = 0.5 + let strokeWidth: CGFloat = 1.0 context.setLineWidth(strokeWidth) context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) @@ -248,9 +249,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) - private let textInputViewRealInsets = UIEdgeInsets(top: 5.5, left: 0.0, bottom: 6.5, right: 0.0) + private let textInputViewRealInsets = UIEdgeInsets(top: 4.5, left: 0.0, bottom: 5.5, right: 0.0) private let accessoryButtonSpacing: CGFloat = 0.0 - private let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel + private let accessoryButtonInset: CGFloat = 2.0 init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { self.textInputContainer = ASDisplayNode() @@ -274,6 +275,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.addSubnode(self.actionButtons) + self.actionButtons.micButton.recordingDisabled = { [weak self] in + self?.interfaceInteraction?.displayRestrictedInfo(.mediaRecording) + } + self.actionButtons.micButton.beginRecording = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction { let isVideo: Bool @@ -485,7 +490,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) - let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + var minimalHeight: CGFloat = 14.0 + textFieldMinHeight + if case .regular = metrics.widthClass, case .regular = metrics.heightClass { + minimalHeight += 2.0 + } return minimalHeight } @@ -929,6 +937,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { hideMicButton = true } + let mediaInputDisabled: Bool + if let bannedRights = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.bannedRights, bannedRights.flags.contains(.banSendMedia) { + mediaInputDisabled = true + } else { + mediaInputDisabled = false + } + self.actionButtons.micButton.fadeDisabled = mediaInputDisabled + self.updateActionButtons(hasText: hasText, hideMicButton: hideMicButton, animated: transition.isAnimated) return panelHeight @@ -1050,10 +1066,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } else { - if self.actionButtons.micButton.alpha.isZero { - self.actionButtons.micButton.alpha = 1.0 + let micAlpha: CGFloat = self.actionButtons.micButton.fadeDisabled ? 0.5 : 1.0 + if !self.actionButtons.micButton.alpha.isEqual(to: micAlpha) { + self.actionButtons.micButton.alpha = micAlpha if animated { - self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: micAlpha, duration: 0.1) if animateWithBounce { self.actionButtons.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) } else { @@ -1305,10 +1322,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { for (item, currentButton) in self.accessoryItemButtons { if currentButton === button { switch item { - case .stickers: + case let .stickers(enabled): + if enabled { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(mode: .other, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) - }) + return (.media(mode: .other, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) + } else { + self.interfaceInteraction?.displayRestrictedInfo(.stickers) + } case .keyboard: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.text, state.keyboardButtonsMessage?.id) @@ -1356,5 +1377,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } return nil } + + func frameForStickersButton() -> CGRect? { + for (item, button) in self.accessoryItemButtons { + if case .stickers = item { + return button.frame.insetBy(dx: 0.0, dy: 6.0) + } + } + return nil + } } diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 7b68c85b10..f2c23985f9 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -200,8 +200,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { switch self.networkState { case .waitingForNetwork: statusNode.title = self.strings.State_WaitingForNetwork - case let .connecting(proxy): - statusNode.title = proxy != nil ? self.strings.State_ConnectingToProxy : self.strings.State_Connecting + case .connecting: + statusNode.title = self.strings.State_Connecting case .updating: statusNode.title = self.strings.State_Updating case .online: @@ -229,7 +229,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if peerView.peerId == self.account.peerId { string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) } else { - string = NSAttributedString(string: peer.displayTitle(or: self.strings.Peer_DeletedUser), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + string = NSAttributedString(string: peer.displayTitle(strings: self.strings), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) } } if peerView.peerId.namespace == Namespaces.Peer.SecretChat { @@ -475,7 +475,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { let transition: ContainedViewLayoutTransition = .immediate self.button.frame = clearBounds - self.contentContainer.frame = clearBounds//CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + self.contentContainer.frame = clearBounds var leftIconWidth: CGFloat = 0.0 var rightIconWidth: CGFloat = 0.0 @@ -484,7 +484,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if self.titleLeftIconNode.supernode == nil { self.contentContainer.addSubnode(titleLeftIconNode) } - leftIconWidth = image.size.width + 3.0 + leftIconWidth = image.size.width + 6.0 } else if self.titleLeftIconNode.supernode != nil { self.titleLeftIconNode.removeFromSupernode() } @@ -519,6 +519,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if titleFrame.size.width < size.width { titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) } + titleFrame.origin.x = max(titleFrame.origin.x, clearBounds.minX + leftIconWidth) self.titleNode.frame = titleFrame var infoFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) diff --git a/TelegramUI/ComponentsThemes.swift b/TelegramUI/ComponentsThemes.swift index c3a4afbe1e..6e87ccaebe 100644 --- a/TelegramUI/ComponentsThemes.swift +++ b/TelegramUI/ComponentsThemes.swift @@ -47,7 +47,7 @@ public extension AlertControllerTheme { } convenience init(authTheme: AuthorizationTheme) { - self.init(backgroundColor: authTheme.backgroundColor, separatorColor: authTheme.separatorColor, highlightedItemColor: authTheme.itemHighlightedBackgroundColor, primaryColor: authTheme.primaryColor, secondaryColor: authTheme.textPlaceholderColor, accentColor: authTheme.accentColor, destructiveColor: authTheme.destructiveColor) + self.init(backgroundColor: authTheme.alertBackgroundColor, separatorColor: authTheme.separatorColor, highlightedItemColor: authTheme.itemHighlightedBackgroundColor, primaryColor: authTheme.primaryColor, secondaryColor: authTheme.textPlaceholderColor, accentColor: authTheme.accentColor, destructiveColor: authTheme.destructiveColor) } } diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index edff5313d9..4fd9adc6c7 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -97,11 +97,14 @@ final class ComposeControllerNode: ASDisplayNode { self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) - self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + insets.top += 20.0 + } } + + self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) } func activateSearch() { diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 9114cc0e1f..bf8364e81b 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -61,10 +61,15 @@ private final class ContactListNodeInteraction { } } +private struct ContactListPeer { + let peer: Peer + let isGlobal: Bool +} + private enum ContactListNodeEntry: Comparable, Identifiable { case search(PresentationTheme, PresentationStrings) case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings) - case peer(Int, Peer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) + case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) var stableId: ContactListNodeEntryId { switch self { @@ -73,7 +78,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case let .option(index, _, _, _): return .option(index: index) case let .peer(_, peer, _, _, _, _, _): - return .peerId(peer.id.toInt64()) + return .peerId(peer.peer.id.toInt64()) } } @@ -87,13 +92,15 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) case let .peer(_, peer, presence, header, selection, theme, strings): let status: ContactsPeerItemStatus - if let presence = presence { + if peer.isGlobal, let _ = peer.peer.addressName { + status = .addressName("") + } else if let presence = presence { status = .presence(presence) } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in - interaction.openPeer(peer) + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer.peer, chatPeer: peer.peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + interaction.openPeer(peer.peer) }) } } @@ -118,7 +125,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsIndex != rhsIndex { return false } - if lhsPeer.id != rhsPeer.id { + if lhsPeer.peer.id != rhsPeer.peer.id { + return false + } + if lhsPeer.isGlobal != rhsPeer.isGlobal { return false } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { @@ -207,18 +217,18 @@ private extension PeerIndexNameRepresentation { } } -private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] - var orderedPeers: [Peer] + var orderedPeers: [ContactListPeer] var headers: [PeerId: ContactListNameIndexHeader] = [:] switch presentation { case let .orderedByPresence(options): entries.append(.search(theme, strings)) orderedPeers = peers.sorted(by: { lhs, rhs in - let lhsPresence = presences[lhs.id] - let rhsPresence = presences[rhs.id] + let lhsPresence = presences[lhs.peer.id] + let rhsPresence = presences[rhs.peer.id] if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { if lhsPresence.status < rhsPresence.status { return false @@ -230,16 +240,16 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences } else if let _ = rhsPresence { return false } - return lhs.id < rhs.id + return lhs.peer.id < rhs.peer.id }) for i in 0 ..< options.count { entries.append(.option(i, options[i], theme, strings)) } case let .natural(displaySearch, options): orderedPeers = peers.sorted(by: { lhs, rhs in - let result = lhs.indexName.isLessThan(other: rhs.indexName) + let result = lhs.peer.indexName.isLessThan(other: rhs.peer.indexName) if result == .orderedSame { - return lhs.id < rhs.id + return lhs.peer.id < rhs.peer.id } else { return result == .orderedAscending } @@ -247,7 +257,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences var headerCache: [unichar: ContactListNameIndexHeader] = [:] for peer in orderedPeers { var indexHeader: unichar = 35 - switch peer.indexName { + switch peer.peer.indexName { case let .title(title, _): if let c = title.utf16.first { indexHeader = c @@ -266,7 +276,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences header = ContactListNameIndexHeader(theme: theme, letter: indexHeader) headerCache[indexHeader] = header } - headers[peer.id] = header + headers[peer.peer.id] = header } if displaySearch { entries.append(.search(theme, strings)) @@ -280,7 +290,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences var removeIndices: [Int] = [] for i in 0 ..< orderedPeers.count { - switch orderedPeers[i].indexName { + switch orderedPeers[i].peer.indexName { case let .title(title, _): if title.isEmpty { removeIndices.append(i) @@ -308,7 +318,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences for i in 0 ..< orderedPeers.count { let selection: ContactsPeerItemSelection if let selectionState = selectionState { - selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].id] != nil) + selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].peer.id] != nil) } else { selection = .none } @@ -317,9 +327,9 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences case .orderedByPresence: header = commonHeader default: - header = headers[orderedPeers[i].id] + header = headers[orderedPeers[i].peer.id] } - entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], header, selection, theme, strings)) + entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].peer.id], header, selection, theme, strings)) } return entries } @@ -474,9 +484,37 @@ final class ContactListNode: ASDisplayNode { if case let .search(query) = presentation { transition = query |> mapToSignal { query in - return combineLatest(account.postbox.searchContacts(query: query), selectionStateSignal, themeAndStringsPromise.get()) - |> mapToQueue { peers, selectionState, themeAndStrings -> Signal in + let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) + let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = + .single(([], [])) + |> then( + searchPeers(account: account, query: query) + |> map { ($0.0, $0.1) } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + + return combineLatest(foundLocalContacts, foundRemoteContacts, selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { localPeers, remotePeers, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in + var peers: [ContactListPeer] = localPeers.map({ ContactListPeer(peer: $0, isGlobal: false) }) + var existingPeerIds = Set(peers.map { $0.peer.id }) + for peer in remotePeers.0 { + if peer.peer is TelegramUser { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) + } + } + } + for peer in remotePeers.1 { + if peer.peer is TelegramUser { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) + } + } + } + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false)) @@ -493,7 +531,7 @@ final class ContactListNode: ASDisplayNode { transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get()) |> mapToQueue { view, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in - let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) + let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers.map({ ContactListPeer(peer: $0, isGlobal: false) }), presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) let previous = previousEntries.swap(entries) let animated: Bool if let previous = previous { diff --git a/TelegramUI/ContactMultiselectionController.swift b/TelegramUI/ContactMultiselectionController.swift index 8b3ffc2b02..748d2e7b4d 100644 --- a/TelegramUI/ContactMultiselectionController.swift +++ b/TelegramUI/ContactMultiselectionController.swift @@ -79,8 +79,8 @@ public class ContactMultiselectionController: ViewController { } }) - self.limitsConfigurationDisposable = (account.postbox.modify { modifier -> LimitsConfiguration in - return currentLimitsConfiguration(modifier: modifier) + self.limitsConfigurationDisposable = (account.postbox.transaction { transaction -> LimitsConfiguration in + return currentLimitsConfiguration(transaction: transaction) } |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { strongSelf.limitsConfiguration = value diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index 16e552ff44..38eddc6b56 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -66,6 +66,13 @@ final class ContactSelectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + insets.top += 20.0 + } + } + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) diff --git a/TelegramUI/ContactSynchronizationSettings.swift b/TelegramUI/ContactSynchronizationSettings.swift new file mode 100644 index 0000000000..b82ababcef --- /dev/null +++ b/TelegramUI/ContactSynchronizationSettings.swift @@ -0,0 +1,45 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct ContactSynchronizationSettings: Equatable, PreferencesEntry { + public var synchronizeDeviceContacts: Bool + + public static var defaultSettings: ContactSynchronizationSettings { + return ContactSynchronizationSettings(synchronizeDeviceContacts: true) + } + + public init(synchronizeDeviceContacts: Bool) { + self.synchronizeDeviceContacts = synchronizeDeviceContacts + } + + public init(decoder: PostboxDecoder) { + self.synchronizeDeviceContacts = decoder.decodeInt32ForKey("synchronizeDeviceContacts", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.synchronizeDeviceContacts ? 1 : 0, forKey: "synchronizeDeviceContacts") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ContactSynchronizationSettings { + return self == to + } else { + return false + } + } +} + +func updateContactSynchronizationSettingsInteractively(postbox: Postbox, _ f: @escaping (ContactSynchronizationSettings) -> ContactSynchronizationSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.contactSynchronizationSettings, { entry in + let currentSettings: ContactSynchronizationSettings + if let entry = entry as? ContactSynchronizationSettings { + currentSettings = entry + } else { + currentSettings = .defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 292f6f60b8..5a2b362227 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -520,10 +520,10 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if item.editing.editable { - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } else { - strongSelf.setRevealOptions([]) + strongSelf.setRevealOptions((left: [], right: [])) } } }) diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 934d4ba643..cdd69aa362 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -5,8 +5,54 @@ import SwiftSignalKit import Postbox import TelegramCore -private enum ContactListSearchEntry { - case peer(Peer, PresentationTheme, PresentationStrings) +private struct ContactListSearchEntry: Identifiable, Comparable { + let index: Int + let peer: Peer + let enabled: Bool + + var stableId: PeerId { + return self.peer.id + } + + static func ==(lhs: ContactListSearchEntry, rhs: ContactListSearchEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if !arePeersEqual(lhs.peer, rhs.peer) { + return false + } + if lhs.enabled != rhs.enabled { + return false + } + return true + } + + static func <(lhs: ContactListSearchEntry, rhs: ContactListSearchEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (Peer) -> Void) -> ListViewItem { + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in + openPeer(peer) + }) + } +} + +struct ContactListSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool +} + +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (Peer) -> Void) -> ContactListSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, openPeer: openPeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, openPeer: openPeer), directionHint: nil) } + + return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { @@ -22,6 +68,9 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private var presentationData: PresentationData private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + private var containerViewLayout: (ContainerViewLayout, CGFloat)? + private var enqueuedTransitions: [ContactListSearchContainerTransition] = [] + init(account: Account, onlyWriteable: Bool, openPeer: @escaping (PeerId) -> Void) { self.account = account self.openPeer = openPeer @@ -33,6 +82,8 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.isHidden = true super.init() @@ -47,15 +98,61 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { let themeAndStringsPromise = self.themeAndStringsPromise let searchItems = searchQuery.get() - |> mapToSignal { query -> Signal<[ContactListSearchEntry], NoError> in + |> mapToSignal { query -> Signal<[ContactListSearchEntry]?, NoError> in if let query = query, !query.isEmpty { - return combineLatest(account.postbox.searchContacts(query: query.lowercased()), themeAndStringsPromise.get()) + let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) + let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = + .single(([], [])) + |> then( + searchPeers(account: account, query: query) + |> map { ($0.0, $0.1) } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + + return combineLatest(foundLocalContacts, foundRemoteContacts, themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) - |> map { peers, themeAndStrings -> [ContactListSearchEntry] in - return peers.map({ .peer($0, themeAndStrings.0, themeAndStrings.1) }) + |> map { localPeers, remotePeers, themeAndStrings -> [ContactListSearchEntry] in + var entries: [ContactListSearchEntry] = [] + var existingPeerIds = Set() + var index = 0 + for peer in localPeers { + existingPeerIds.insert(peer.id) + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer) + } + entries.append(ContactListSearchEntry(index: index, peer: peer, enabled: enabled)) + index += 1 + } + for peer in remotePeers.1 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer.peer) + } + + entries.append(ContactListSearchEntry(index: index, peer: peer.peer, enabled: enabled)) + index += 1 + } + } + for peer in remotePeers.0 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer.peer) + } + + entries.append(ContactListSearchEntry(index: index, peer: peer.peer, enabled: enabled)) + index += 1 + } + } + return entries } } else { - return .single([]) + return .single(nil) } } @@ -64,16 +161,20 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { self.searchDisposable.set((searchItems |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { - let previousItems = previousSearchItems.swap(items) + let previousItems = previousSearchItems.swap(items ?? []) - var listItems: [ListViewItem] = [] + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, openPeer: { peer in + if let openPeer = self?.openPeer { + self?.listNode.clearHighlightAnimated(true) + openPeer(peer.id) + } + }) + + /*var listItems: [ListViewItem] = [] for item in items { switch item { case let .peer(peer, theme, strings): - var enabled = true - if onlyWriteable { - enabled = canSendMessagesToPeer(peer) - } + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { @@ -82,16 +183,9 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { } })) } - } + }*/ - let isEmpty = listItems.isEmpty - - strongSelf.listNode.transaction(deleteIndices: (0 ..< previousItems.count).map({ ListViewDeleteItem(index: $0, directionHint: nil) }), insertIndicesAndItems: (0 ..< listItems.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: listItems[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil, completion: { _ in - if let strongSelf = self { - strongSelf.listNode.isHidden = isEmpty - strongSelf.backgroundColor = isEmpty ? UIColor.black.withAlphaComponent(0.5) : strongSelf.presentationData.theme.chatList.backgroundColor - } - }) + strongSelf.enqueueTransition(transition) } })) @@ -121,11 +215,45 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + let hadValidLayout = self.containerViewLayout != nil + self.containerViewLayout = (layout, navigationBarHeight) + let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func enqueueTransition(_ transition: ContactListSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.containerViewLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + self?.dimNode.isHidden = isSearching + }) + } } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/TelegramUI/CountryList.swift b/TelegramUI/CountryList.swift index 2c701b849d..9aa3ba1266 100644 --- a/TelegramUI/CountryList.swift +++ b/TelegramUI/CountryList.swift @@ -12,7 +12,8 @@ private func loadCountriesInfo() -> [(Int, String, String)] { } let delimiter = ";" - let endOfLine = "\n" + let endOfLine1 = "\r\n" + let endOfLine2 = "\n" var array: [(Int, String, String)] = [] @@ -33,7 +34,18 @@ private func loadCountriesInfo() -> [(Int, String, String)] { let countryId = String(data[codeRange.upperBound ..< idRange.lowerBound]) let countryName: String - let nameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) + let nameRange1 = data.range(of: endOfLine1, options: [], range: idRange.upperBound ..< data.endIndex) + let nameRange2 = data.range(of: endOfLine2, options: [], range: idRange.upperBound ..< data.endIndex) + var nameRange: Range? + if let nameRange1 = nameRange1, let nameRange2 = nameRange2 { + if nameRange1.lowerBound < nameRange2.lowerBound { + nameRange = nameRange1 + } else { + nameRange = nameRange2 + } + } else { + nameRange = nameRange1 ?? nameRange2 + } if let nameRange = nameRange { countryName = String(data[idRange.upperBound ..< nameRange.lowerBound]) currentLocation = nameRange.upperBound diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index b36221e39b..94c26112da 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -343,14 +343,14 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.storageUsage(presentationData.theme, presentationData.strings.Cache_Title)) entries.append(.networkUsage(presentationData.theme, presentationData.strings.NetworkUsageSettings_Title)) - entries.append(.automaticMediaDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticMediaDownload)) - entries.append(.automaticDownloadMaster(presentationData.theme, presentationData.strings.ChatSettings_AutomaticMediaDownloadMaster, data.automaticMediaDownloadSettings.masterEnabled)) - entries.append(.automaticDownloadPhoto(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadPhoto)) - entries.append(.automaticDownloadVideo(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVideo)) - entries.append(.automaticDownloadFile(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadFile)) - entries.append(.automaticDownloadVoiceMessage(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVoiceMessage)) - entries.append(.automaticDownloadVideoMessage(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVideoMessage)) - entries.append(.automaticDownloadReset(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadReset, data.automaticMediaDownloadSettings.peers != AutomaticMediaDownloadSettings.defaultSettings.peers || !data.automaticMediaDownloadSettings.masterEnabled)) + entries.append(.automaticMediaDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadTitle)) + entries.append(.automaticDownloadMaster(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadEnabled, data.automaticMediaDownloadSettings.masterEnabled)) + entries.append(.automaticDownloadPhoto(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadPhotos)) + entries.append(.automaticDownloadVideo(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadVideos)) + entries.append(.automaticDownloadFile(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadDocuments)) + entries.append(.automaticDownloadVoiceMessage(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadVoiceMessages)) + entries.append(.automaticDownloadVideoMessage(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadVideoMessages)) + entries.append(.automaticDownloadReset(presentationData.theme, presentationData.strings.ChatSettings_AutoDownloadReset, data.automaticMediaDownloadSettings.peers != AutomaticMediaDownloadSettings.defaultSettings.peers || !data.automaticMediaDownloadSettings.masterEnabled)) entries.append(.voiceCallsHeader(presentationData.theme, presentationData.strings.Settings_CallSettings.uppercased())) entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(strings: presentationData.strings, settings: data.voiceCallSettings))) diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift index 033faeace6..c062ab9028 100644 --- a/TelegramUI/DebugAccountsController.swift +++ b/TelegramUI/DebugAccountsController.swift @@ -101,13 +101,13 @@ public func debugAccountsController(account: Account, accountManager: AccountMan let arguments = DebugAccountsControllerArguments(account: account, presentController: { controller, arguments in presentControllerImpl?(controller, arguments) }, switchAccount: { id in - let _ = accountManager.modify({ modifier -> Void in - modifier.setCurrentId(id) + let _ = accountManager.transaction({ transaction -> Void in + transaction.setCurrentId(id) }).start() }, loginNewAccount: { - let _ = accountManager.modify({ modifier -> Void in - let id = modifier.createRecord([]) - modifier.setCurrentId(id) + let _ = accountManager.transaction({ transaction -> Void in + let id = transaction.createRecord([]) + transaction.setCurrentId(id) }).start() }) diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index 5edac8bb53..dd441bca2e 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -32,6 +32,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case logToFile(PresentationTheme, Bool) case logToConsole(PresentationTheme, Bool) case enableRaiseToSpeak(PresentationTheme, Bool) + case keepChatNavigationStack(PresentationTheme, Bool) var section: ItemListSectionId { switch self { @@ -43,7 +44,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.payments.rawValue case .logToFile, .logToConsole: return DebugControllerSection.logging.rawValue - case .enableRaiseToSpeak: + case .enableRaiseToSpeak, .keepChatNavigationStack: return DebugControllerSection.experiments.rawValue } } @@ -62,6 +63,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 4 case .enableRaiseToSpeak: return 5 + case .keepChatNavigationStack: + return 6 } } @@ -103,6 +106,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .keepChatNavigationStack(lhsTheme, lhsValue): + if case let .keepChatNavigationStack(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -142,13 +151,13 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) case let .logToFile(theme, value): return ItemListSwitchItem(theme: theme, title: "Log to File", value: value, sectionId: self.section, style: .blocks, updated: { value in - updateLoggingSettings(postbox: arguments.account.postbox, { + let _ = updateLoggingSettings(postbox: arguments.account.postbox, { $0.withUpdatedLogToFile(value) }).start() }) case let .logToConsole(theme, value): return ItemListSwitchItem(theme: theme, title: "Log to Console", value: value, sectionId: self.section, style: .blocks, updated: { value in - updateLoggingSettings(postbox: arguments.account.postbox, { + let _ = updateLoggingSettings(postbox: arguments.account.postbox, { $0.withUpdatedLogToConsole(value) }).start() }) @@ -158,11 +167,19 @@ private enum DebugControllerEntry: ItemListNodeEntry { $0.withUpdatedEnableRaiseToSpeak(value) }).start() }) + case let .keepChatNavigationStack(theme, value): + return ItemListSwitchItem(theme: theme, title: "Keep Chat Stack", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = updateExperimentalUISettingsInteractively(postbox: arguments.account.postbox, { settings in + var settings = settings + settings.keepChatNavigationStack = value + return settings + }).start() + }) } } } -private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings) -> [DebugControllerEntry] { +private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] entries.append(.sendLogs(presentationData.theme)) @@ -173,6 +190,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) + entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) return entries } @@ -187,7 +205,7 @@ public func debugController(account: Account, accountManager: AccountManager) -> pushControllerImpl?(controller) }) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [PreferencesKeys.loggingSettings, ApplicationSpecificPreferencesKeys.mediaInputSettings])) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [PreferencesKeys.loggingSettings, ApplicationSpecificPreferencesKeys.mediaInputSettings, ApplicationSpecificPreferencesKeys.experimentalUISettings])) |> map { presentationData, preferencesView -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in let loggingSettings: LoggingSettings if let value = preferencesView.values[PreferencesKeys.loggingSettings] as? LoggingSettings { @@ -203,8 +221,10 @@ public func debugController(account: Account, accountManager: AccountManager) -> mediaInputSettings = MediaInputSettings.defaultSettings } + let experimentalSettings: ExperimentalUISettings = (preferencesView.values[ApplicationSpecificPreferencesKeys.experimentalUISettings] as? ExperimentalUISettings) ?? ExperimentalUISettings.defaultSettings + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings), style: .blocks) + let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 8afbc8c756..0a949749de 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -11,13 +11,16 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(AutomaticMediaDownloadSettings.self, f: { AutomaticMediaDownloadSettings(decoder: $0) }) 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) }) + declareEncodable(ApplicationSpecificVariantNotice.self, f: { ApplicationSpecificVariantNotice(decoder: $0) }) declareEncodable(CallListSettings.self, f: { CallListSettings(decoder: $0) }) declareEncodable(ExperimentalSettings.self, f: { ExperimentalSettings(decoder: $0) }) + declareEncodable(ExperimentalUISettings.self, f: { ExperimentalUISettings(decoder: $0) }) declareEncodable(MusicPlaybackSettings.self, f: { MusicPlaybackSettings(decoder: $0) }) declareEncodable(ICloudFileResource.self, f: { ICloudFileResource(decoder: $0) }) declareEncodable(MediaInputSettings.self, f: { MediaInputSettings(decoder: $0) }) + declareEncodable(ContactSynchronizationSettings.self, f: { ContactSynchronizationSettings(decoder: $0) }) + declareEncodable(CachedChannelAdminIds.self, f: { CachedChannelAdminIds(decoder: $0) }) return }() diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index ac427410d8..cb9adfb6ee 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -77,12 +77,15 @@ private let list = PresentationThemeList( freeTextColor: UIColor(rgb: 0x82888E), freeTextErrorColor: destructiveColor, //!!! freeTextSuccessColor: UIColor(rgb: 0x30cf30), //!!! + freeMonoIcon: UIColor(rgb: 0x82888E), itemSwitchColors: switchColors, itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x415A71), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x374F63), foregroundColor: .white), destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white), - constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white), + accent: PresentationThemeItemDisclosureAction(fillColor: accentColor, foregroundColor: .white), + warning: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x3c4e61), foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), @@ -231,13 +234,13 @@ private let inputMediaPanel = PresentationThemeInputMediaPanel( panelSerapatorColor: UIColor(rgb: 0x213040), panelIconColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), panelHighlightedIconBackgroundColor: UIColor(rgb: 0x131C26), //!!! - stickersBackgroundColor: UIColor(rgb: 0x131C26), + stickersBackgroundColor: UIColor(rgb: 0x18222d), stickersSectionTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), - stickersSearchBackgroundColor: UIColor(rgb: 0x151c25), - stickersSearchPlaceholderColor: UIColor(rgb: 0x7b8995), + stickersSearchBackgroundColor: UIColor(rgb: 0x121c25), + stickersSearchPlaceholderColor: UIColor(rgb: 0x788a96), stickersSearchPrimaryColor: .white, - stickersSearchControlColor: UIColor(rgb: 0x7b8995), - gifsBackgroundColor: UIColor(rgb: 0x131C26) + stickersSearchControlColor: UIColor(rgb: 0x788a96), + gifsBackgroundColor: UIColor(rgb: 0x18222d) ) private let inputButtonPanel = PresentationThemeInputButtonPanel( diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index 73cede6a6a..effd21b525 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -77,12 +77,15 @@ private let list = PresentationThemeList( freeTextColor: UIColor(rgb: 0x8d8e93), freeTextErrorColor: UIColor(rgb: 0xcf3030), //!!! freeTextSuccessColor: UIColor(rgb: 0x30cf30), //!!! + freeMonoIcon: UIColor(rgb: 0x8d8e93), itemSwitchColors: switchColors, itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x414141), foregroundColor: .white), destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white), - constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white), + accent: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), + warning: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x414141), foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 2da2477601..43589721db 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -77,12 +77,15 @@ private let list = PresentationThemeList( freeTextColor: UIColor(rgb: 0x6d6d72), freeTextErrorColor: UIColor(rgb: 0xcf3030), freeTextSuccessColor: UIColor(rgb: 0x26972c), + freeMonoIcon: UIColor(rgb: 0x7e7e87), itemSwitchColors: switchColors, itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xaaaab3), foregroundColor: .white), destructive: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xff3824), foregroundColor: .white), - constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white), + accent: PresentationThemeItemDisclosureAction(fillColor: accentColor, foregroundColor: .white), + warning: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xff9500), foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xC7C7CC), @@ -306,17 +309,17 @@ private let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMed ) private let inputPanel = PresentationThemeChatInputPanel( - panelBackgroundColor: UIColor(rgb: 0xf2f4f6), - panelStrokeColor: UIColor(rgb: 0xbdc2c7), + panelBackgroundColor: UIColor(rgb: 0xf7f7f7), + panelStrokeColor: UIColor(rgb: 0xb2b2b2), panelControlAccentColor: accentColor, - panelControlColor: UIColor(rgb: 0x727b87), + panelControlColor: UIColor(rgb: 0x858e99), panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5), panelControlDestructiveColor: UIColor(rgb: 0xff3b30), inputBackgroundColor: UIColor(rgb: 0xffffff), - inputStrokeColor: UIColor(rgb: 0xd3d6da), + inputStrokeColor: UIColor(rgb: 0xd9dcdf), inputPlaceholderColor: UIColor(rgb: 0xbebec0), inputTextColor: .black, - inputControlColor: UIColor(rgb: 0x9099A2, alpha: 0.6), + inputControlColor: UIColor(rgb: 0xa0a7b0), actionControlFillColor: accentColor, actionControlForegroundColor: .white, primaryTextColor: .black, @@ -328,9 +331,9 @@ private let inputPanel = PresentationThemeChatInputPanel( private let inputMediaPanel = PresentationThemeInputMediaPanel( panelSerapatorColor: UIColor(rgb: 0xBEC2C6), - panelIconColor: UIColor(rgb: 0x9099A2), - panelHighlightedIconBackgroundColor: UIColor(rgb: 0x9099A2, alpha: 0.2), - stickersBackgroundColor: UIColor(rgb: 0xE8EBF0), + panelIconColor: UIColor(rgb: 0x858e99), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x858e99, alpha: 0.2), + stickersBackgroundColor: UIColor(rgb: 0xe8ebf0), stickersSectionTextColor: UIColor(rgb: 0x9099A2), stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1), stickersSearchPlaceholderColor: UIColor(rgb: 0x8e8e93), diff --git a/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift b/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift new file mode 100644 index 0000000000..a84162022d --- /dev/null +++ b/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift @@ -0,0 +1,81 @@ +import Foundation +import TelegramCore +import AsyncDisplayKit +import Display + +final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { + private let containerNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let textNode: ImmediateTextNode + + private var validLayout: (CGSize, CGFloat, CGFloat)? + + override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.containerNode = ASDisplayNode() + self.separatorNode = ASDisplayNode() + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 0 + self.textNode.textAlignment = .center + + super.init(account: account, theme: theme, strings: strings) + + self.isOpaque = false + self.clipsToBounds = true + + self.containerNode.addSubnode(self.textNode) + self.containerNode.addSubnode(self.separatorNode) + self.addSubnode(self.containerNode) + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + let firstLayout = self.validLayout == nil + + self.validLayout = (size, leftInset, rightInset) + + self.containerNode.backgroundColor = interfaceState.theme.list.plainBackgroundColor + self.separatorNode.backgroundColor = interfaceState.theme.list.itemPlainSeparatorColor + + guard let bannedRights = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.bannedRights else { + return + } + let banDescription: String + if bannedRights.untilDate != 0 && bannedRights.untilDate != Int32.max { + banDescription = interfaceState.strings.Conversation_RestrictedInlineTimed(stringForFullDate(timestamp: bannedRights.untilDate, strings: interfaceState.strings, timeFormat: .regular)).0 + } else { + banDescription = interfaceState.strings.Conversation_RestrictedInline + } + + self.textNode.attributedText = NSAttributedString(string: banDescription, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + + let verticalInset: CGFloat = 8.0 + let textSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset - rightInset - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + floor((size.width - leftInset - rightInset - textSize.width) / 2.0), y: verticalInset), size: textSize) + + let containerHeight = textSize.height + verticalInset * 2.0 + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - containerHeight), size: CGSize(width: size.width, height: containerHeight)) + transition.updateFrame(node: self.containerNode, frame: containerFrame) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) + + if firstLayout { + self.animateIn() + } + } + + func animateIn() { + let position = self.containerNode.layer.position + self.containerNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (self.containerNode.bounds.height)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + + override func animateOut(completion: @escaping () -> Void) { + let position = self.containerNode.layer.position + self.containerNode.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.containerNode.bounds.height)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let containerFrame = self.containerNode.frame + return self.containerNode.hitTest(CGPoint(x: point.x - containerFrame.minX, y: point.y - containerFrame.minY), with: event) + } +} diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index ace76a4904..2e6291ff44 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -14,11 +14,14 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let textNode: ASTextNode let imageNode: TransformImageNode - let activityIndicator: ActivityIndicator + private let activityIndicator: ActivityIndicator + private let statusNode: RadialStatusNode private let messageDisposable = MetaDisposable() private let editingMessageDisposable = MetaDisposable() + private var currentMessage: Message? + private var currentEditMedia: Media? private var previousMedia: Media? override var interfaceInteraction: ChatPanelInterfaceInteraction? { @@ -26,12 +29,17 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let statuses = self.interfaceInteraction?.statuses { self.editingMessageDisposable.set(statuses.editingMessage.start(next: { [weak self] value in if let strongSelf = self { - if value { - if strongSelf.activityIndicator.supernode == nil { - strongSelf.addSubnode(strongSelf.activityIndicator) + if let value = value { + if value.isZero { + strongSelf.activityIndicator.isHidden = false + strongSelf.statusNode.transitionToState(.none, completion: {}) + } else { + strongSelf.activityIndicator.isHidden = true + strongSelf.statusNode.transitionToState(.progress(color: strongSelf.theme.chat.inputPanel.panelControlAccentColor, value: CGFloat(value), cancelEnabled: false), completion: {}) } - } else if strongSelf.activityIndicator.supernode != nil { - strongSelf.activityIndicator.removeFromSupernode() + } else { + strongSelf.activityIndicator.isHidden = true + strongSelf.statusNode.transitionToState(.none, completion: {}) } } })) @@ -39,11 +47,15 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } } + private let account: Account var theme: PresentationTheme + var strings: PresentationStrings init(account: Account, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings) { + self.account = account self.messageId = messageId self.theme = theme + self.strings = strings self.closeButton = ASButtonNode() self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) @@ -64,12 +76,17 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.textNode.truncationMode = .byTruncatingTail self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = true self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true + self.imageNode.isUserInteractionEnabled = true self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlAccentColor, 22.0, 2.0)) + self.activityIndicator.isHidden = true + + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) super.init() @@ -80,96 +97,12 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) + self.addSubnode(self.activityIndicator) + self.addSubnode(self.statusNode) self.messageDisposable.set((account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in - if let strongSelf = self { - var text = "" - if let message = message { - (text, _) = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId) - } - - var updatedMedia: Media? - var imageDimensions: CGSize? - if let message = message, !message.containsSecretMedia { - for media in message.media { - if let image = media as? TelegramMediaImage { - updatedMedia = image - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions - } - break - } else if let file = media as? TelegramMediaFile { - updatedMedia = file - if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { - imageDimensions = representation.dimensions - } - break - } - } - } - - let imageNodeLayout = strongSelf.imageNode.asyncLayout() - var applyImage: (() -> Void)? - if let imageDimensions = imageDimensions { - let boundingSize = CGSize(width: 35.0, height: 35.0) - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) - } - - var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = strongSelf.previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (strongSelf.previousMedia != nil) { - mediaUpdated = true - } - strongSelf.previousMedia = updatedMedia - - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - if mediaUpdated { - if let updatedMedia = updatedMedia, imageDimensions != nil { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: account, photo: image) - } else if let file = updatedMedia as? TelegramMediaFile { - if file.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: account, file: file) - } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) - updateImageSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage) - } - } - } else { - updateImageSignal = .single({ _ in return nil }) - } - } - - let isMedia: Bool - if let message = message { - switch messageContentKind(message, strings: strings, accountPeerId: account.peerId) { - case .text: - isMedia = false - default: - isMedia = true - } - } else { - isMedia = false - } - - strongSelf.titleNode.attributedText = NSAttributedString(string: strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) - - if let applyImage = applyImage { - applyImage() - strongSelf.imageNode.isHidden = false - } else { - strongSelf.imageNode.isHidden = true - } - - if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(updateImageSignal) - } - - strongSelf.setNeedsLayout() - } + self?.updateMessage(message) })) } @@ -178,6 +111,129 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.editingMessageDisposable.dispose() } + override func didLoad() { + super.didLoad() + + self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))) + self.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))) + } + + + + private func updateMessage(_ message: Message?) { + self.currentMessage = message + + var text = "" + if let message = message { + var effectiveMessage = message + if let currentEditMedia = self.currentEditMedia { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMedia]) + } + (text, _) = descriptionStringForMessage(effectiveMessage, strings: self.strings, accountPeerId: self.account.peerId) + } + + var updatedMedia: Media? + var imageDimensions: CGSize? + if let message = message, !message.containsSecretMedia { + var candidateMedia: Media? + if let currentEditMedia = self.currentEditMedia { + candidateMedia = currentEditMedia + } else { + for media in message.media { + if media is TelegramMediaImage || media is TelegramMediaFile { + candidateMedia = media + break + } + } + } + + if let image = candidateMedia as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions + } + } else if let file = candidateMedia as? TelegramMediaFile { + updatedMedia = file + if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions + } + } + } + + let imageNodeLayout = self.imageNode.asyncLayout() + var applyImage: (() -> Void)? + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 35.0, height: 35.0) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + } + + var mediaUpdated = false + if let updatedMedia = updatedMedia, let previousMedia = self.previousMedia { + mediaUpdated = !updatedMedia.isEqual(previousMedia) + } else if (updatedMedia != nil) != (self.previousMedia != nil) { + mediaUpdated = true + } + self.previousMedia = updatedMedia + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if mediaUpdated { + if let updatedMedia = updatedMedia, imageDimensions != nil { + if let image = updatedMedia as? TelegramMediaImage { + updateImageSignal = chatMessagePhotoThumbnail(account: self.account, photo: image) + } else if let file = updatedMedia as? TelegramMediaFile { + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: self.account, file: file) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], reference: nil) + updateImageSignal = chatWebpageSnippetPhoto(account: self.account, photo: tmpImage) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + + let isMedia: Bool + if let message = message { + var effectiveMessage = message + if let currentEditMedia = self.currentEditMedia { + effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMedia]) + } + switch messageContentKind(effectiveMessage, strings: strings, accountPeerId: self.account.peerId) { + case .text: + isMedia = false + default: + isMedia = true + } + } else { + isMedia = false + } + + self.titleNode.attributedText = NSAttributedString(string: self.strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) + + if let applyImage = applyImage { + applyImage() + self.imageNode.isHidden = false + } else { + self.imageNode.isHidden = true + } + + if let updateImageSignal = updateImageSignal { + self.imageNode.setSignal(.single({ arguments in + /*let context = DrawingContext(size: arguments.boundingSize) + context.withContext { c in + c.setFillColor(UIColor.white.cgColor) + c.fill(CGRect(origin: CGPoint(), size: context.size)) + } + return context*/ + return nil + }) |> then(updateImageSignal)) + } + + self.setNeedsLayout() + } + override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme { self.theme = theme @@ -202,6 +258,22 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { return CGSize(width: constrainedSize.width, height: 45.0) } + override func updateState(size: CGSize, interfaceState: ChatPresentationInterfaceState) { + let editMedia = interfaceState.editMessageState?.media + var updatedEditMedia = false + if let currentEditMedia = self.currentEditMedia, let editMedia = editMedia { + if !currentEditMedia.isEqual(editMedia) { + updatedEditMedia = true + } + } else if (editMedia != nil) != (currentEditMedia != nil) { + updatedEditMedia = true + } + if updatedEditMedia { + self.currentEditMedia = editMedia + self.updateMessage(self.currentMessage) + } + } + override func layout() { super.layout() @@ -212,7 +284,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let textRightInset: CGFloat = 20.0 let indicatorSize = CGSize(width: 22.0, height: 22.0) - activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) + self.statusNode.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize).insetBy(dx: -2.0, dy: -2.0) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) @@ -237,4 +310,10 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { dismiss() } } + + @objc func imageTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.interfaceInteraction?.setupEditMessageMedia() + } + } } diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index ee6061de97..f55149d676 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -296,8 +296,8 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName wallpapersPromise.set(telegramWallpapers(account: account)) let changeProfilePhotoImpl: () -> Void = { - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(account.peerId) + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(account.peerId) } |> deliverOnMainQueue).start(next: { peer in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -472,7 +472,7 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } dismissImpl = { [weak controller] in - (controller?.navigationController as? NavigationController)?.popViewController(animated: true) + let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { diff --git a/TelegramUI/EmojisChatInputContextPanelNode.swift b/TelegramUI/EmojisChatInputContextPanelNode.swift index 7c3b816623..a28c57b52e 100644 --- a/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -95,7 +95,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { interfaceInteraction.updateTextInputState { textInputState in var hashtagQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { - if type == [.stickerSearch] { + if type == [.emojiSearch] { var range = range range.location -= 1 range.length += 1 diff --git a/TelegramUI/ExperimentalSettings.swift b/TelegramUI/ExperimentalSettings.swift index f18d1a13dd..f2b627ffab 100644 --- a/TelegramUI/ExperimentalSettings.swift +++ b/TelegramUI/ExperimentalSettings.swift @@ -35,8 +35,8 @@ public struct ExperimentalSettings: PreferencesEntry, Equatable { } func updateExperimentalSettingsInteractively(postbox: Postbox, _ f: @escaping (ExperimentalSettings) -> ExperimentalSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { entry in let currentSettings: ExperimentalSettings if let entry = entry as? ExperimentalSettings { currentSettings = entry diff --git a/TelegramUI/ExperimentalUISettings.swift b/TelegramUI/ExperimentalUISettings.swift new file mode 100644 index 0000000000..7c47b588f5 --- /dev/null +++ b/TelegramUI/ExperimentalUISettings.swift @@ -0,0 +1,45 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct ExperimentalUISettings: Equatable, PreferencesEntry { + public var keepChatNavigationStack: Bool + + public static var defaultSettings: ExperimentalUISettings { + return ExperimentalUISettings(keepChatNavigationStack: false) + } + + public init(keepChatNavigationStack: Bool) { + self.keepChatNavigationStack = keepChatNavigationStack + } + + public init(decoder: PostboxDecoder) { + self.keepChatNavigationStack = decoder.decodeInt32ForKey("keepChatNavigationStack", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.keepChatNavigationStack ? 1 : 0, forKey: "keepChatNavigationStack") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ExperimentalUISettings { + return self == to + } else { + return false + } + } +} + +func updateExperimentalUISettingsInteractively(postbox: Postbox, _ f: @escaping (ExperimentalUISettings) -> ExperimentalUISettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.experimentalUISettings, { entry in + let currentSettings: ExperimentalUISettings + if let entry = entry as? ExperimentalUISettings { + currentSettings = entry + } else { + currentSettings = .defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index c4ca922875..d73f00c323 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -166,10 +166,12 @@ public func featuredStickerPacksController(account: Account) -> ViewController { let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) + var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + let arguments = FeaturedStickerPacksControllerArguments(account: account, openStickerPack: { info in - presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentStickerPackController?(info) }, addPack: { info in - presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentStickerPackController?(info) }) let stickerPacks = Promise() @@ -231,5 +233,9 @@ public func featuredStickerPacksController(account: Account) -> ViewController { } } + presentStickerPackController = { [weak controller] info in + presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + return controller } diff --git a/TelegramUI/FormControllerDetailActionItem.swift b/TelegramUI/FormControllerDetailActionItem.swift index a1a50bc8fe..9e67ed798f 100644 --- a/TelegramUI/FormControllerDetailActionItem.swift +++ b/TelegramUI/FormControllerDetailActionItem.swift @@ -3,17 +3,20 @@ import AsyncDisplayKit import Display private let textFont = Font.regular(17.0) +private let errorFont = Font.regular(13.0) final class FormControllerDetailActionItem: FormControllerItem { let title: String let text: String let placeholder: String + let error: String? let activated: () -> Void - init(title: String, text: String, placeholder: String, activated: @escaping () -> Void) { + init(title: String, text: String, placeholder: String, error: String?, activated: @escaping () -> Void) { self.title = title self.text = text self.placeholder = placeholder + self.error = error self.activated = activated } @@ -35,6 +38,7 @@ final class FormControllerDetailActionItem: FormControllerItem { final class FormControllerDetailActionItemNode: FormBlockItemNode { private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode + private let errorNode: ImmediateTextNode private var item: FormControllerDetailActionItem? @@ -47,11 +51,15 @@ final class FormControllerDetailActionItemNode: FormBlockItemNode (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) { @@ -80,7 +88,18 @@ final class FormControllerDetailActionItemNode: FormBlockItemNode Void - init(title: String, text: String, placeholder: String, textUpdated: @escaping (String) -> Void) { + init(title: String, text: String, placeholder: String, error: String? = nil, textUpdated: @escaping (String) -> Void) { self.title = title self.text = text self.placeholder = placeholder + self.error = error self.textUpdated = textUpdated } @@ -34,14 +37,20 @@ final class FormControllerTextInputItem: FormControllerItem { final class FormControllerTextInputItemNode: FormBlockItemNode { private let titleNode: ImmediateTextNode + private let errorNode: ImmediateTextNode private let textField: TextFieldNode private var item: FormControllerTextInputItem? init() { self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 + self.errorNode = ImmediateTextNode() + self.errorNode.displaysAsynchronously = false + self.errorNode.maximumNumberOfLines = 0 + self.textField = TextFieldNode() self.textField.textField.font = textFont self.textField.textField.returnKeyType = .next @@ -49,6 +58,7 @@ final class FormControllerTextInputItemNode: FormBlockItemNode CGFloat { - let baseWidth: CGFloat = 20.0 + let baseWidth: CGFloat = 23.0 let boundingSize = self.imageSize.aspectFilled(CGSize(width: 1.0, height: height)) let width = baseWidth * (1.0 - progress) + boundingSize.width * progress let arguments = TransformImageArguments(corners: ImageCorners(radius: 0), imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) @@ -121,8 +121,8 @@ final class GalleryThumbnailContainerNode: ASDisplayNode { self.currentLayout = size self.contentNode.frame = CGRect(origin: CGPoint(), size: size) let spacing: CGFloat = 2.0 - let centralSpacing: CGFloat = 6.0 - let itemHeight: CGFloat = 30.0 + let centralSpacing: CGFloat = 8.0 + let itemHeight: CGFloat = 42.0 var itemFrames: [CGRect] = [] var lastTrailingSpacing: CGFloat = 0.0 diff --git a/TelegramUI/GeneratedMediaStoreSettings.swift b/TelegramUI/GeneratedMediaStoreSettings.swift index 2ca6918bfa..a5bb8f0954 100644 --- a/TelegramUI/GeneratedMediaStoreSettings.swift +++ b/TelegramUI/GeneratedMediaStoreSettings.swift @@ -39,8 +39,8 @@ public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable { } func updateGeneratedMediaStoreSettingsInteractively(postbox: Postbox, _ f: @escaping (GeneratedMediaStoreSettings) -> GeneratedMediaStoreSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, { entry in let currentSettings: GeneratedMediaStoreSettings if let entry = entry as? GeneratedMediaStoreSettings { currentSettings = entry diff --git a/TelegramUI/GlobalExperimentalSettings.swift b/TelegramUI/GlobalExperimentalSettings.swift index 143bfa6a53..f536ee5f67 100644 --- a/TelegramUI/GlobalExperimentalSettings.swift +++ b/TelegramUI/GlobalExperimentalSettings.swift @@ -3,4 +3,5 @@ import Foundation public struct GlobalExperimentalSettings { public static var isAppStoreBuild: Bool = false public static var enableFeed: Bool = false + public static var enablePassport: Bool = false } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 91cc2c7a55..7f711085ea 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -25,6 +25,8 @@ private final class GroupInfoArguments { let updateEditingDescriptionText: (String) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let addMember: () -> Void + let promotePeer: (RenderedChannelParticipant) -> Void + let restrictPeer: (RenderedChannelParticipant) -> Void let removePeer: (PeerId) -> Void let convertToSupergroup: () -> Void let leave: () -> Void @@ -32,7 +34,7 @@ private final class GroupInfoArguments { let displayAboutContextMenu: (String) -> Void let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, togglePreHistory: @escaping (Bool) -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { + init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, togglePreHistory: @escaping (Bool) -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) { self.account = account self.peerId = peerId self.avatarAndNameInfoContext = avatarAndNameInfoContext @@ -49,6 +51,8 @@ private final class GroupInfoArguments { self.updateEditingDescriptionText = updateEditingDescriptionText self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.addMember = addMember + self.promotePeer = promotePeer + self.restrictPeer = restrictPeer self.removePeer = removePeer self.convertToSupergroup = convertToSupergroup self.leave = leave @@ -108,6 +112,18 @@ private enum GroupEntryStableId: Hashable, Equatable { } } +enum ParticipantRevealActionType { + case promote + case restrict + case remove +} + +private struct ParticipantRevealAction: Equatable { + let type: ItemListPeerItemRevealOptionType + let title: String + let action: ParticipantRevealActionType +} + private enum GroupInfoEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setGroupPhoto(PresentationTheme, String) @@ -124,7 +140,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case membersAdmins(PresentationTheme, String, String) case membersBlacklist(PresentationTheme, String, String) case addMember(PresentationTheme, String, editing: Bool) - case member(PresentationTheme, PresentationStrings, index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) + case member(PresentationTheme, PresentationStrings, index: Int, peerId: PeerId, peer: Peer, participant: RenderedChannelParticipant?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, revealActions: [ParticipantRevealAction], enabled: Bool) case convertToSupergroup(PresentationTheme, String) case leave(PresentationTheme, String) @@ -286,8 +302,8 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case let .member(lhsTheme, lhsStrings, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): - if case let .member(rhsTheme, rhsStrings, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { + case let .member(lhsTheme, lhsStrings, lhsIndex, lhsPeerId, lhsPeer, lhsParticipant, lhsPresence, lhsMemberStatus, lhsEditing, lhsActions, lhsEnabled): + if case let .member(rhsTheme, rhsStrings, rhsIndex, rhsPeerId, rhsPeer, rhsParticipant, rhsPresence, rhsMemberStatus, rhsEditing, rhsActions, rhsEnabled) = rhs { if lhsTheme !== rhsTheme { return false } @@ -306,6 +322,9 @@ private enum GroupInfoEntry: ItemListNodeEntry { if !lhsPeer.isEqual(rhsPeer) { return false } + if lhsParticipant != rhsParticipant { + return false + } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { if !lhsPresence.isEqual(to: rhsPresence) { return false @@ -316,6 +335,9 @@ private enum GroupInfoEntry: ItemListNodeEntry { if lhsEditing != rhsEditing { return false } + if lhsActions != rhsActions { + return false + } if lhsEnabled != rhsEnabled { return false } @@ -328,7 +350,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { var stableId: GroupEntryStableId { switch self { - case let .member(_, _, _, peerId, _, _, _, _, _): + case let .member(_, _, _, peerId, _, _, _, _, _, _, _): return .peer(peerId) default: return .index(self.sortIndex) @@ -367,7 +389,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 13 case .addMember: return 14 - case let .member(_, _, index, _, _, _, _, _, _): + case let .member(_, _, index, _, _, _, _, _, _, _, _): return 20 + index case .convertToSupergroup: return 100000 @@ -444,7 +466,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.pushController(channelBlacklistController(account: arguments.account, peerId: arguments.peerId)) }) - case let .member(theme, strings, _, _, peer, presence, memberStatus, editing, enabled): + case let .member(theme, strings, _, _, peer, participant, presence, memberStatus, editing, actions, enabled): let label: String? switch memberStatus { case .admin: @@ -452,7 +474,24 @@ private enum GroupInfoEntry: ItemListNodeEntry { case .member: label = nil } - return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + var options: [ItemListPeerItemRevealOption] = [] + for action in actions { + options.append(ItemListPeerItemRevealOption(type: action.type, title: action.title, action: { + switch action.action { + case .promote: + if let participant = participant { + arguments.promotePeer(participant) + } + case .restrict: + if let participant = participant { + arguments.restrictPeer(participant) + } + case .remove: + arguments.removePeer(peer.id) + } + })) + } + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, sectionId: self.section, action: { if let infoController = peerInfoController(account: arguments.account, peer: peer) { arguments.pushController(infoController) } @@ -608,7 +647,7 @@ private func canRemoveParticipant(account: Account, isAdmin: Bool, participantId return isAdmin } -private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, globalNotificationSettings: GlobalNotificationSettings, state: GroupInfoState) -> [GroupInfoEntry] { +private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, channelMembers: [RenderedChannelParticipant], globalNotificationSettings: GlobalNotificationSettings, state: GroupInfoState) -> [GroupInfoEntry] { var entries: [GroupInfoEntry] = [] var highlightAdmins = false @@ -705,7 +744,14 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa if canViewAdminsAndBanned { entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, presentationData.strings.Channel_Info_Management, cachedChannelData.participantsSummary.adminCount.flatMap { "\($0)" } ?? "")) - entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, presentationData.strings.Channel_Info_Banned, cachedChannelData.participantsSummary.bannedCount.flatMap { "\($0)" } ?? "" )) + var restrictedAndBannedCount: Int32 = 0 + if let restrictedCount = cachedChannelData.participantsSummary.bannedCount { + restrictedAndBannedCount += restrictedCount + } + if let bannedCount = cachedChannelData.participantsSummary.kickedCount { + restrictedAndBannedCount += bannedCount + } + entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, presentationData.strings.Channel_Info_Banned, restrictedAndBannedCount != 0 ? "\(restrictedAndBannedCount)" : "" )) } } } else { @@ -830,81 +876,110 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } else { memberStatus = .member } - entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: peer.id, peer: peer, participant: nil, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), revealActions: [ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)], enabled: !disabledPeerIds.contains(peer.id))) } } - } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { - var updatedParticipants = participants.participants - let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) - var peerPresences: [PeerId: PeerPresence] = view.peerPresences - var peers: [PeerId: Peer] = view.peers - var disabledPeerIds = state.removingParticipantIds + } else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + let updatedParticipants = channelMembers + let disabledPeerIds = state.removingParticipantIds - if !state.temporaryParticipants.isEmpty { - for participant in state.temporaryParticipants { - if !existingParticipantIds.contains(participant.peer.id) { - updatedParticipants.append(.member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil)) - if let presence = participant.presence, peerPresences[participant.peer.id] == nil { - peerPresences[participant.peer.id] = presence + let sortedParticipants: [RenderedChannelParticipant] + if memberCount < 200 { + sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in + let lhsPresence = lhs.presences[lhs.peer.id] as? TelegramUserPresence + let rhsPresence = rhs.presences[rhs.peer.id] as? TelegramUserPresence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true } - if peers[participant.peer.id] == nil { - peers[participant.peer.id] = participant.peer - } - disabledPeerIds.insert(participant.peer.id) - } - } - } - - let sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in - let lhsPresence = peerPresences[lhs.peerId] as? TelegramUserPresence - let rhsPresence = peerPresences[rhs.peerId] as? TelegramUserPresence - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - if lhsPresence.status < rhsPresence.status { - return false - } else if lhsPresence.status > rhsPresence.status { + } else if let _ = lhsPresence { return true - } - } else if let _ = lhsPresence { - return true - } else if let _ = rhsPresence { - return false - } - - switch lhs { - case .creator: + } else if let _ = rhsPresence { return false - case let .member(lhsId, lhsInvitedAt, _, _): - switch rhs { - case .creator: - return true - case let .member(rhsId, rhsInvitedAt, _, _): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - } - } - }) + } + + switch lhs.participant { + case .creator: + return false + case let .member(lhsId, lhsInvitedAt, _, _): + switch rhs.participant { + case .creator: + return true + case let .member(rhsId, rhsInvitedAt, _, _): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + } + }) + } else { + sortedParticipants = updatedParticipants + } for i in 0 ..< sortedParticipants.count { - if let peer = peers[sortedParticipants[i].peerId] { - let memberStatus: GroupInfoMemberStatus - if highlightAdmins { - switch sortedParticipants[i] { - case .creator: + let participant = sortedParticipants[i] + let memberStatus: GroupInfoMemberStatus + if highlightAdmins { + switch participant.participant { + case .creator: + memberStatus = .admin + case let .member(_, _, adminInfo, _): + if adminInfo != nil { memberStatus = .admin - case let .member(_, _, adminInfo, _): - if adminInfo != nil { - memberStatus = .admin - } else { - memberStatus = .member - } - } - } else { - memberStatus = .member + } else { + memberStatus = .member + } } - entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + } else { + memberStatus = .member } + + var canPromote: Bool + var canRestrict: Bool + switch participant.participant { + case .creator: + canPromote = false + canRestrict = false + case let .member(_, _, adminRights, bannedRights): + if channel.hasAdminRights([.canAddAdmins]) { + canPromote = true + } else { + canPromote = false + } + if channel.hasAdminRights([.canBanUsers]) { + canRestrict = true + } else { + canRestrict = false + } + if canPromote { + if let bannedRights = bannedRights { + if bannedRights.restrictedBy != account.peerId && !channel.flags.contains(.isCreator) { + canPromote = false + } + } + } + if canRestrict { + if let adminRights = adminRights { + if adminRights.promotedBy != account.peerId && !channel.flags.contains(.isCreator) { + canRestrict = false + } + } + } + } + + var peerActions: [ParticipantRevealAction] = [] + if canPromote { + peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote)) + } + if canRestrict { + peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict)) + peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) + } + + entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: participant.peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(participant.peer.id))) } } @@ -1026,8 +1101,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl })) }) }, changeProfilePhoto: { - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -1160,9 +1235,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, changeNotificationSoundSettings: { - let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in - let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings - let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + let _ = (account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings return (peerSettings, globalSettings) } |> deliverOnMainQueue).start(next: { settings in let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.groupChats.sound, completion: { sound in @@ -1240,10 +1315,15 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl return result.get() } } + let addMember = contactsController.result |> deliverOnMainQueue |> mapToSignal { memberId -> Signal in if let memberId = memberId { + if peerId.namespace == Namespaces.Peer.CloudChannel { + return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.addMember(account: account, peerId: peerId, memberId: memberId) + } + return account.postbox.peerView(id: memberId) |> take(1) |> deliverOnMainQueue @@ -1307,6 +1387,12 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) addMemberDisposable.set(addMember.start()) }) + }, promotePeer: { participant in + presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, restrictPeer: { participant in + presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removePeer: { memberId in let signal = account.postbox.loadedPeerWithId(memberId) |> deliverOnMainQueue @@ -1334,6 +1420,20 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds).withUpdatedRemovingParticipantIds(removingParticipantIds) } + if peerId.namespace == Namespaces.Peer.CloudChannel { + return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: memberId, bannedRights: TelegramChannelBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> afterDisposed { + Queue.mainQueue().async { + updateState { state in + var removingParticipantIds = state.removingParticipantIds + removingParticipantIds.remove(memberId) + + return state.withUpdatedRemovingParticipantIds(removingParticipantIds) + } + } + } + } + return removePeerMember(account: account, peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> afterDisposed { @@ -1379,9 +1479,24 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl aboutLinkActionImpl?(action, itemLink) }) + var loadMoreControl: PeerChannelMemberCategoryControl? + + let channelMembersPromise = Promise<[RenderedChannelParticipant]>() + if peerId.namespace == Namespaces.Peer.CloudChannel { + let (disposable, control) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, updated: { state in + channelMembersPromise.set(.single(state.list)) + }) + loadMoreControl = control + actionsDisposable.add(disposable) + } else { + channelMembersPromise.set(.single([])) + } + + let previousChannelMemberCount = Atomic(value: nil) + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [globalNotificationsKey])) - |> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [globalNotificationsKey]), channelMembersPromise.get()) + |> map { presentationData, state, view, combinedView, channelMembers -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var globalNotificationSettings: GlobalNotificationSettings = GlobalNotificationSettings.defaultSettings @@ -1484,7 +1599,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .blocks, searchItem: searchItem) + let previousCount = previousChannelMemberCount.swap(channelMembers.count) ?? 0 + let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, channelMembers: channelMembers, globalNotificationSettings: globalNotificationSettings, state: state), style: .blocks, searchItem: searchItem, animateChanges: previousCount >= channelMembers.count) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -1563,6 +1679,11 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } } } + controller.visibleBottomContentOffsetChanged = { offset in + if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 { + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + } return controller } @@ -1592,7 +1713,7 @@ func handlePeerInfoAboutTextAction(account: Account, peerId: PeerId, navigateDis navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), messageId: messageId) } case let .stickerPack(name): - controller.present(StickerPackPreviewController(account: account, stickerPack: .name(name)), in: .window(.root)) + controller.present(StickerPackPreviewController(account: account, stickerPack: .name(name), parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) case let .instantView(webpage, anchor): (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) case let .join(link): @@ -1691,7 +1812,7 @@ func handlePeerInfoAboutTextAction(account: Account, peerId: PeerId, navigateDis ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) - ])]) + ])]) controller.present(actionSheet, in: .window(.root)) } } diff --git a/TelegramUI/GroupInfoSearchItem.swift b/TelegramUI/GroupInfoSearchItem.swift index 247722e0b6..fa67cc3d51 100644 --- a/TelegramUI/GroupInfoSearchItem.swift +++ b/TelegramUI/GroupInfoSearchItem.swift @@ -50,7 +50,7 @@ private final class GroupInfoSearchItemNode: ItemListControllerSearchNode { private let containerNode: ChannelMembersSearchContainerNode init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void, cancel: @escaping () -> Void) { - self.containerNode = ChannelMembersSearchContainerNode(account: account, peerId: peerId, mode: .searchMembers, openPeer: { peer in + self.containerNode = ChannelMembersSearchContainerNode(account: account, peerId: peerId, mode: .searchMembers, openPeer: { peer, _ in openPeer(peer) }) self.containerNode.cancel = { diff --git a/TelegramUI/GroupsInCommonController.swift b/TelegramUI/GroupsInCommonController.swift index 1b476f56bd..a84bef3400 100644 --- a/TelegramUI/GroupsInCommonController.swift +++ b/TelegramUI/GroupsInCommonController.swift @@ -24,8 +24,8 @@ private enum GroupsInCommonEntryStableId: Hashable { var hashValue: Int { switch self { - case let .peer(peerId): - return peerId.hashValue + case let .peer(peerId): + return peerId.hashValue } } @@ -141,10 +141,10 @@ public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewCo }) let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(groupsInCommon(account: account, peerId: peerId) |> mapToSignal { peerIds -> Signal<[Peer], NoError> in - return account.postbox.modify { modifier -> [Peer] in + return account.postbox.transaction { transaction -> [Peer] in var result: [Peer] = [] for id in peerIds { - if let peer = modifier.getPeer(id) { + if let peer = transaction.getPeer(id) { result.append(peer) } } diff --git a/TelegramUI/HashtagSearchController.swift b/TelegramUI/HashtagSearchController.swift index aaa6017739..6ba9d2e61f 100644 --- a/TelegramUI/HashtagSearchController.swift +++ b/TelegramUI/HashtagSearchController.swift @@ -59,6 +59,7 @@ final class HashtagSearchController: TelegramController { }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in + }, togglePeerMarkedUnread: { _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) @@ -67,7 +68,7 @@ final class HashtagSearchController: TelegramController { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, account: account, enableHeaders: false, onlyWriteable: false, interaction: interaction) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, account: account, enableHeaders: false, filter: [], interaction: interaction) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) diff --git a/TelegramUI/IconSwitchNode.swift b/TelegramUI/IconSwitchNode.swift new file mode 100644 index 0000000000..f37982b6eb --- /dev/null +++ b/TelegramUI/IconSwitchNode.swift @@ -0,0 +1,91 @@ +import Foundation +import AsyncDisplayKit + +import LegacyComponents + +private final class IconSwitchNodeViewLayer: CALayer { + override func setNeedsDisplay() { + } +} + +private final class IconSwitchNodeView: TGIconSwitchView { + override class var layerClass: AnyClass { + return IconSwitchNodeViewLayer.self + } +} + +class IconSwitchNode: ASDisplayNode { + public var valueUpdated: ((Bool) -> Void)? + + public var frameColor = UIColor(rgb: 0xe0e0e0) { + didSet { + if self.isNodeLoaded { + (self.view as! UISwitch).tintColor = self.frameColor + } + } + } + public var handleColor = UIColor(rgb: 0xffffff) { + didSet { + if self.isNodeLoaded { + //(self.view as! UISwitch).thumbTintColor = self.handleColor + } + } + } + public var contentColor = UIColor(rgb: 0x42d451) { + didSet { + if self.isNodeLoaded { + (self.view as! UISwitch).onTintColor = self.contentColor + } + } + } + + private var _isOn: Bool = false + public var isOn: Bool { + get { + return self._isOn + } set(value) { + if (value != self._isOn) { + self._isOn = value + if self.isNodeLoaded { + (self.view as! UISwitch).setOn(value, animated: false) + } + } + } + } + + override public init() { + super.init() + + self.setViewBlock({ + return IconSwitchNodeView() + }) + } + + override open func didLoad() { + super.didLoad() + + (self.view as! UISwitch).backgroundColor = self.backgroundColor + (self.view as! UISwitch).tintColor = self.frameColor + //(self.view as! UISwitch).thumbTintColor = self.handleColor + (self.view as! UISwitch).onTintColor = self.contentColor + + (self.view as! UISwitch).setOn(self._isOn, animated: false) + + (self.view as! UISwitch).addTarget(self, action: #selector(switchValueChanged(_:)), for: .valueChanged) + } + + public func setOn(_ value: Bool, animated: Bool) { + self._isOn = value + if self.isNodeLoaded { + (self.view as! UISwitch).setOn(value, animated: animated) + } + } + + override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 51.0, height: 31.0) + } + + @objc func switchValueChanged(_ view: UISwitch) { + self.valueUpdated?(view.isOn) + } +} diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 7577191554..c930612c4d 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -80,8 +80,8 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { } func updateInAppNotificationSettingsInteractively(postbox: Postbox, _ f: @escaping (InAppNotificationSettings) -> InAppNotificationSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings, { entry in let currentSettings: InAppNotificationSettings if let entry = entry as? InAppNotificationSettings { currentSettings = entry diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index cf5a4c2cf1..23cd7f4f10 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -299,7 +299,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati var entries: [InstalledStickerPacksEntry] = [] switch mode { - case .general: + case .general, .modal: if featured.count != 0 { var unreadCount: Int32 = 0 for item in featured { @@ -312,7 +312,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati entries.append(.archived(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedPacks)) entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) entries.append(.packsTitle(presentationData.theme, presentationData.strings.StickerPacksSettings_StickerPacksSection)) - case .masks, .modal: + case .masks: break } @@ -362,8 +362,10 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) + var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + let arguments = InstalledStickerPacksControllerArguments(account: account, openStickerPack: { info in - presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentStickerPackController?(info) }, setPackIdWithRevealedOptions: { packId, fromPackId in updateState { state in if (packId == nil && fromPackId == state.packIdWithRevealedOptions) || (packId != nil && fromPackId == nil) { @@ -406,9 +408,9 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti let featured = Promise<[FeaturedStickerPackItem]>() switch mode { - case .general: + case .general, .modal: featured.set(account.viewTracker.featuredStickerPacks()) - case .masks, .modal: + case .masks: featured.set(.single([])) } @@ -422,37 +424,49 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti packCount = entries.count } - var leftNavigationButton: ItemListNavigationButton? - if case .modal = mode { + let leftNavigationButton: ItemListNavigationButton? = nil + /*if case .modal = mode { leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) - } + }*/ var rightNavigationButton: ItemListNavigationButton? if let packCount = packCount, packCount != 0 { - if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - updateState { - $0.withUpdatedEditing(false) - } - if case .modal = mode { - dismissImpl?() - } - }) + if case .modal = mode { + rightNavigationButton = nil } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { - updateState { - $0.withUpdatedEditing(true) - } - }) + if state.editing { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + updateState { + $0.withUpdatedEditing(false) + } + if case .modal = mode { + dismissImpl?() + } + }) + } else { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { + updateState { + $0.withUpdatedEditing(true) + } + }) + } } } let previous = previousPackCount previousPackCount = packCount - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(mode == .general ? presentationData.strings.StickerPacksSettings_Title : presentationData.strings.MaskStickerSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let title: String + switch mode { + case .general, .modal: + title = presentationData.strings.StickerPacksSettings_Title + case .masks: + title = presentationData.strings.MaskStickerSettings_Title + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, featured: featured), style: .blocks, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) @@ -485,8 +499,8 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti afterAll = true } - let _ = (account.postbox.modify { modifier -> Void in - var infos = modifier.getItemCollectionsInfos(namespace: namespaceForMode(mode)) + let _ = (account.postbox.transaction { transaction -> Void in + var infos = transaction.getItemCollectionsInfos(namespace: namespaceForMode(mode)) var reorderInfo: ItemCollectionInfo? for i in 0 ..< infos.count { if infos[i].0 == fromPackInfo.id { @@ -517,8 +531,8 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } else if afterAll { infos.append((fromPackInfo.id, reorderInfo)) } - modifier.replaceItemCollectionInfos(namespace: namespaceForMode(mode), itemCollectionInfos: infos) - addSynchronizeInstalledStickerPacksOperation(modifier: modifier, namespace: namespaceForMode(mode)) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespaceForMode(mode)) + transaction.replaceItemCollectionInfos(namespace: namespaceForMode(mode), itemCollectionInfos: infos) } }).start() } @@ -528,6 +542,9 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti controller.present(c, in: .window(.root), with: p) } } + presentStickerPackController = { [weak controller] info in + presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index b0d41d9f8f..ca03e57f84 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -5,6 +5,28 @@ import SwiftSignalKit import Postbox import TelegramCore +private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem { + let account: Account + let representations: [TelegramMediaImageRepresentation] + + var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: self.representations, reference: nil) + if let representation = largestImageRepresentation(image.representations) { + return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions) + } else { + return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) + } + } + + func isEqual(to: GalleryThumbnailItem) -> Bool { + if let to = to as? InstantImageGalleryThumbnailItem { + return self.representations == to.representations + } else { + return false + } + } +} + class InstantImageGalleryItem: GalleryItem { let account: Account let theme: PresentationTheme @@ -43,7 +65,7 @@ class InstantImageGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return nil + return (0, InstantImageGalleryThumbnailItem(account: self.account, representations: self.image.representations)) } } diff --git a/TelegramUI/InstantPagePresentationSettings.swift b/TelegramUI/InstantPagePresentationSettings.swift index 77a199bf8d..ce9d7006ce 100644 --- a/TelegramUI/InstantPagePresentationSettings.swift +++ b/TelegramUI/InstantPagePresentationSettings.swift @@ -88,8 +88,8 @@ final class InstantPagePresentationSettings: PreferencesEntry, Equatable { } func updateInstantPagePresentationSettingsInteractively(postbox: Postbox, _ f: @escaping (InstantPagePresentationSettings) -> InstantPagePresentationSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantPagePresentationSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantPagePresentationSettings, { entry in let currentSettings: InstantPagePresentationSettings if let entry = entry as? InstantPagePresentationSettings { currentSettings = entry diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 759c9e952f..c554174fea 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -161,7 +161,6 @@ class ItemListActionItemNode: ListViewItemNode { } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { @@ -242,7 +241,7 @@ class ItemListActionItemNode: ListViewItemNode { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { + if highlighted && self.item?.kind != ItemListActionKind.disabled { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { var anchorNode: ASDisplayNode? diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index b6532cd760..5257b976b3 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -56,7 +56,7 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { } else if !lastName.isEmpty { return lastName } else { - return strings.Peer_DeletedUser + return strings.User_DeletedAccount } case let .title(title, _): return title diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index eb9f7f90bc..8d6cc14b23 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -166,6 +166,14 @@ final class ItemListController: ViewController { } } + var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged + } + } + } + var reorderEntry: ((Int, Int, [Entry]) -> Void)? { didSet { if self.isNodeLoaded { @@ -366,6 +374,7 @@ final class ItemListController: ViewController { } displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated + displayNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged displayNode.reorderEntry = self.reorderEntry self.displayNode = displayNode super.displayNodeDidLoad() diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index e3f5fa787a..e833376b1a 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -115,6 +115,7 @@ class ItemListControllerNode: ViewControllerTracingNod var dismiss: (() -> Void)? var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? + var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? var reorderEntry: ((Int, Int, [Entry]) -> Void)? var enableInteractiveDismiss = false { @@ -162,6 +163,10 @@ class ItemListControllerNode: ViewControllerTracingNod } } + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + self?.visibleBottomContentOffsetChanged?(offset) + } + let previousState = Atomic?>(value: nil) self.transitionDisposable.set(((state |> map { theme, stateAndArguments -> ItemListNodeTransition in let (state, arguments) = stateAndArguments diff --git a/TelegramUI/ItemListEditableItem.swift b/TelegramUI/ItemListEditableItem.swift index afdded4eb0..68f8c2135e 100644 --- a/TelegramUI/ItemListEditableItem.swift +++ b/TelegramUI/ItemListEditableItem.swift @@ -56,8 +56,9 @@ final class ItemListRevealOptionsGestureRecognizer: UIPanGestureRecognizer { class ItemListRevealOptionsItemNode: ListViewItemNode { private var validLayout: (CGSize, CGFloat, CGFloat)? - private var revealNode: ItemListRevealOptionsNode? - private var revealOptions: [ItemListRevealOption] = [] + private var leftRevealNode: ItemListRevealOptionsNode? + private var rightRevealNode: ItemListRevealOptionsNode? + private var revealOptions: (left: [ItemListRevealOption], right: [ItemListRevealOption]) = ([], []) private var initialRevealOffset: CGFloat = 0.0 private(set) var revealOffset: CGFloat = 0.0 @@ -85,34 +86,71 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { self.recognizer = recognizer recognizer.allowAnyDirection = self.allowAnyDirection self.view.addGestureRecognizer(recognizer) + + self.view.disablesInteractiveTransitionGestureRecognizer = self.allowAnyDirection } - func setRevealOptions(_ options: [ItemListRevealOption]) { - let wasEmpty = self.revealOptions.isEmpty + func setRevealOptions(_ options: (left: [ItemListRevealOption], right: [ItemListRevealOption])) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty self.revealOptions = options - if options.isEmpty { - if let _ = self.revealNode { + let isEmpty = options.left.isEmpty && options.right.isEmpty + if options.left.isEmpty { + if let _ = self.leftRevealNode { self.recognizer?.becomeCancelled() self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) } - } else { - if let revealNode = self.revealNode { - revealNode.setOptions(options) + } else if previousOptions.left != options.left { + /*if let _ = self.leftRevealNode { + self.revealOptionsInteractivelyClosed() + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + }*/ + } + if options.right.isEmpty { + if let _ = self.rightRevealNode { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + } + } else if previousOptions.right != options.right { + if let _ = self.rightRevealNode { + /*self.revealOptionsInteractivelyClosed() + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))*/ } } - if wasEmpty != options.isEmpty { - self.recognizer?.isEnabled = !options.isEmpty + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero + if allowAnyDirection != self.allowAnyDirection { + self.allowAnyDirection = allowAnyDirection + self.recognizer?.allowAnyDirection = allowAnyDirection + if self.isNodeLoaded { + self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection + } } } @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { - guard let (size, leftInset, rightInset) = self.validLayout else { + guard let (size, _, _) = self.validLayout else { return } switch recognizer.state { case .began: - if let revealNode = self.revealNode { - let revealSize = revealNode.calculatedSize + if let leftRevealNode = self.leftRevealNode { + let revealSize = leftRevealNode.calculatedSize + let location = recognizer.location(in: self.view) + if location.x < revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else if let rightRevealNode = self.rightRevealNode { + let revealSize = rightRevealNode.calculatedSize let location = recognizer.location(in: self.view) if location.x > size.width - revealSize.width { recognizer.becomeCancelled() @@ -120,7 +158,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { self.initialRevealOffset = self.revealOffset } } else { - if self.revealOptions.isEmpty { + if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty { recognizer.becomeCancelled() } self.initialRevealOffset = self.revealOffset @@ -128,19 +166,51 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { case .changed: var translation = recognizer.translation(in: self.view) translation.x += self.initialRevealOffset - translation.x = min(0.0, translation.x) - if self.revealNode == nil && translation.x.isLess(than: 0.0) { - self.setupAndAddRevealNode() + if self.revealOptions.left.isEmpty { + translation.x = min(0.0, translation.x) + } + if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) { + self.setupAndAddLeftRevealNode() + self.revealOptionsInteractivelyOpened() + } else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRightRevealNode() self.revealOptionsInteractivelyOpened() } self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) - if self.revealNode == nil { + if self.leftRevealNode == nil && self.rightRevealNode == nil { self.revealOptionsInteractivelyClosed() } case .ended, .cancelled: - if let recognizer = self.recognizer, let revealNode = self.revealNode { + guard let recognizer = self.recognizer else { + break + } + + if let leftRevealNode = self.leftRevealNode { let velocity = recognizer.velocity(in: self.view) - let revealSize = revealNode.calculatedSize + let revealSize = leftRevealNode.calculatedSize + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset > 0.0 { + reveal = true + } else if self.revealOffset > revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x > 0.0 { + reveal = true + } else { + reveal = false + } + } + self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + if !reveal { + self.revealOptionsInteractivelyClosed() + } + } else if let rightRevealNode = self.rightRevealNode { + let velocity = recognizer.velocity(in: self.view) + let revealSize = rightRevealNode.calculatedSize var reveal = false if abs(velocity.x) < 100.0 { if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { @@ -167,13 +237,32 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { } } - private func setupAndAddRevealNode() { - if !self.revealOptions.isEmpty { + private func setupAndAddLeftRevealNode() { + if !self.revealOptions.left.isEmpty { let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in self?.revealOptionSelected(option) }) - revealNode.setOptions(self.revealOptions) - self.revealNode = revealNode + revealNode.setOptions(self.revealOptions.left) + self.leftRevealNode = revealNode + + if let (size, _, rightInset) = self.validLayout { + let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + + revealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, rightInset: rightInset, transition: .immediate) + } + + self.addSubnode(revealNode) + } + } + + private func setupAndAddRightRevealNode() { + if !self.revealOptions.right.isEmpty { + let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in + self?.revealOptionSelected(option) + }) + revealNode.setOptions(self.revealOptions.right) + self.rightRevealNode = revealNode if let (size, _, rightInset) = self.validLayout { let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) @@ -189,9 +278,14 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { self.validLayout = (size, leftInset, rightInset) - if let revealNode = self.revealNode { - let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) - revealNode.frame = CGRect(origin: CGPoint(x: size.width - rightInset + max(self.revealOffset, -revealSize.width - rightInset), y: 0.0), size: revealSize) + if let leftRevealNode = self.leftRevealNode { + let revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + leftRevealNode.frame = CGRect(origin: CGPoint(x: leftInset + min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + } + + if let rightRevealNode = self.rightRevealNode { + let revealSize = rightRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + rightRevealNode.frame = CGRect(origin: CGPoint(x: size.width - rightInset + max(self.revealOffset, -revealSize.width - rightInset), y: 0.0), size: revealSize) } } @@ -201,23 +295,39 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { return } - if let revealNode = self.revealNode { - let revealSize = revealNode.calculatedSize + if let leftRevealNode = self.leftRevealNode { + let revealSize = leftRevealNode.calculatedSize + + let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + let revealNodeOffset = max(-self.revealOffset, revealSize.width) + leftRevealNode.updateRevealOffset(offset: revealNodeOffset, rightInset: rightInset, transition: transition) + + if CGFloat(offset).isLessThanOrEqualTo(0.0) { + self.leftRevealNode = nil + transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { [weak leftRevealNode] _ in + leftRevealNode?.removeFromSupernode() + }) + } else { + transition.updateFrame(node: leftRevealNode, frame: revealFrame) + } + } + if let rightRevealNode = self.rightRevealNode { + let revealSize = rightRevealNode.calculatedSize let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) let revealNodeOffset = -max(self.revealOffset, -revealSize.width - rightInset) - revealNode.updateRevealOffset(offset: revealNodeOffset, rightInset: rightInset, transition: transition) + rightRevealNode.updateRevealOffset(offset: revealNodeOffset, rightInset: rightInset, transition: transition) if CGFloat(0.0).isLessThanOrEqualTo(offset) { - self.revealNode = nil - transition.updateFrame(node: revealNode, frame: revealFrame, completion: { [weak revealNode] _ in - revealNode?.removeFromSupernode() + self.rightRevealNode = nil + transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { [weak rightRevealNode] _ in + rightRevealNode?.removeFromSupernode() }) } else { - transition.updateFrame(node: revealNode, frame: revealFrame) + transition.updateFrame(node: rightRevealNode, frame: revealFrame) } } - let allowAnyDirection = !offset.isZero + let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero if allowAnyDirection != self.allowAnyDirection { self.allowAnyDirection = allowAnyDirection self.recognizer?.allowAnyDirection = allowAnyDirection @@ -251,11 +361,11 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { transition = .immediate } if value { - if self.revealNode == nil { - self.setupAndAddRevealNode() - if let revealNode = self.revealNode, revealNode.isNodeLoaded, let (_, _, rightInset) = self.validLayout { - revealNode.layout() - let revealSize = revealNode.calculatedSize + if self.rightRevealNode == nil { + self.setupAndAddRightRevealNode() + if let rightRevealNode = self.rightRevealNode, rightRevealNode.isNodeLoaded, let (_, _, rightInset) = self.validLayout { + rightRevealNode.layout() + let revealSize = rightRevealNode.calculatedSize self.updateRevealOffsetInternal(offset: -revealSize.width - rightInset, transition: transition) } } diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 43629af69e..5632099715 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -38,6 +38,22 @@ enum ItemListPeerItemAliasHandling { case threatSelfAsSaved } +enum ItemListPeerItemRevealOptionType { + case neutral + case warning + case destructive +} + +struct ItemListPeerItemRevealOption { + let type: ItemListPeerItemRevealOptionType + let title: String + let action: () -> Void +} + +struct ItemListPeerItemRevealOptions { + let options: [ItemListPeerItemRevealOption] +} + final class ItemListPeerItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings @@ -48,6 +64,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let text: ItemListPeerItemText let label: ItemListPeerItemLabel let editing: ItemListPeerItemEditing + let revealOptions: ItemListPeerItemRevealOptions? let switchValue: ItemListPeerItemSwitch? let enabled: Bool let sectionId: ItemListSectionId @@ -56,7 +73,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account @@ -66,6 +83,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { self.text = text self.label = label self.editing = editing + self.revealOptions = revealOptions self.switchValue = switchValue self.enabled = enabled self.sectionId = sectionId @@ -233,7 +251,30 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + if let revealOptions = item.revealOptions { + var mappedOptions: [ItemListRevealOption] = [] + var index: Int32 = 0 + for option in revealOptions.options { + let color: UIColor + let textColor: UIColor + switch option.type { + case .neutral: + color = item.theme.list.itemDisclosureActions.constructive.fillColor + textColor = item.theme.list.itemDisclosureActions.constructive.foregroundColor + case .warning: + color = item.theme.list.itemDisclosureActions.warning.fillColor + textColor = item.theme.list.itemDisclosureActions.warning.foregroundColor + case .destructive: + color = item.theme.list.itemDisclosureActions.destructive.fillColor + textColor = item.theme.list.itemDisclosureActions.destructive.foregroundColor + } + mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: nil, color: color, textColor: textColor)) + index += 1 + } + peerRevealOptions = mappedOptions + } else { + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + } } else { peerRevealOptions = [] } @@ -543,7 +584,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions(peerRevealOptions) + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } }) @@ -650,7 +691,13 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() if let (item, _, _) = self.layoutParams { - item.removePeer(item.peer.id) + if let revealOptions = item.revealOptions { + if option.key >= 0 && option.key < Int32(revealOptions.options.count) { + revealOptions.options[Int(option.key)].action() + } + } else { + item.removePeer(item.peer.id) + } } } diff --git a/TelegramUI/ItemListRecentSessionItem.swift b/TelegramUI/ItemListRecentSessionItem.swift index db0bc1b478..e45e91136c 100644 --- a/TelegramUI/ItemListRecentSessionItem.swift +++ b/TelegramUI/ItemListRecentSessionItem.swift @@ -357,7 +357,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions(peerRevealOptions) + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) strongSelf.setRevealOptionsOpened(item.revealed, animated: animated) } }) diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index f7ba074b7e..0375a70c3f 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -492,7 +492,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions(packRevealOptions) + strongSelf.setRevealOptions((left: [], right: packRevealOptions)) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) if let updatedFetchSignal = updatedFetchSignal { diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index af5e99bd75..db3505fd48 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -3,20 +3,27 @@ import Display import AsyncDisplayKit import SwiftSignalKit +enum ItemListSwitchItemNodeType { + case regular + case icon +} + class ItemListSwitchItem: ListViewItem, ItemListItem { let theme: PresentationTheme let title: String let value: Bool + let type: ItemListSwitchItemNodeType let enableInteractiveChanges: Bool let enabled: Bool let sectionId: ItemListSectionId let style: ItemListStyle let updated: (Bool) -> Void - init(theme: PresentationTheme, title: String, value: Bool, enableInteractiveChanges: Bool = true, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { + init(theme: PresentationTheme, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { self.theme = theme self.title = title self.value = value + self.type = type self.enableInteractiveChanges = enableInteractiveChanges self.enabled = enabled self.sectionId = sectionId @@ -26,7 +33,7 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { - let node = ItemListSwitchItemNode() + let node = ItemListSwitchItemNode(type: self.type) let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize @@ -62,19 +69,32 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(17.0) +private protocol ItemListSwitchNodeImpl { + var frameColor: UIColor { get set } + var contentColor: UIColor { get set } + var handleColor: UIColor { get set } +} + +extension SwitchNode: ItemListSwitchNodeImpl { + +} + +extension IconSwitchNode: ItemListSwitchNodeImpl { +} + class ItemListSwitchItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let titleNode: TextNode - private var switchNode: SwitchNode + private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl private let switchGestureNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? private var item: ItemListSwitchItem? - init() { + init(type: ItemListSwitchItemNodeType) { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white @@ -87,7 +107,12 @@ class ItemListSwitchItemNode: ListViewItemNode { self.titleNode = TextNode() self.titleNode.isLayerBacked = true - self.switchNode = SwitchNode() + switch type { + case .regular: + self.switchNode = SwitchNode() + case .icon: + self.switchNode = IconSwitchNode() + } self.switchGestureNode = ASDisplayNode() diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 6f9bcf04d5..1b2ba09b64 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -11,6 +11,7 @@ func legacyAttachmentMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, controller.dismissesByOutsideTap = true controller.hasSwipeGesture = true controller.maxHeight = 445.0// - TGMenuSheetButtonItemViewHeight + controller.forceFullScreen = true var itemViews: [Any] = [] let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping)! diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index 1c7ba45ec9..27b2394f3f 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -5,7 +5,7 @@ import UIKit import TelegramCore import Postbox -func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { +func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, saveCapturedPhotos: Bool, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = .portrait @@ -21,7 +21,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen } controller.isImportant = true - controller.shouldStoreCapturedAssets = true + controller.shouldStoreCapturedAssets = saveCapturedPhotos controller.allowCaptions = true controller.inhibitDocumentCaptions = false controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) @@ -60,7 +60,17 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen legacyController?.dismiss() } - controller.finishedWithPhoto = { [weak menuController, weak legacyController] overlayController, image, caption, stickers, timer in + controller.finishedWithResults = { [weak menuController, weak legacyController] overlayController, selectionContext, editingContext, currentItem in + if let selectionContext = selectionContext, let editingContext = editingContext { + let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedPhotos, saveEditedPhotos: saveCapturedPhotos, descriptionGenerator: legacyAssetPickerItemGenerator()) + sendMessagesWithSignals(signals) + } + + menuController?.dismiss(animated: false) + legacyController?.dismissWithAnimation() + } + + controller.finishedWithPhoto = { [weak menuController, weak legacyController] overlayController, image, caption, entities, stickers, timer in if let image = image { let description = NSMutableDictionary() description["type"] = "capturedPhoto" @@ -68,7 +78,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen if let timer = timer { description["timer"] = timer } - if let item = legacyAssetPickerItemGenerator()(description, caption, nil) { + if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil) { sendMessagesWithSignals([SSignal.single(item)]) } } @@ -77,7 +87,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen legacyController?.dismissWithAnimation() } - controller.finishedWithVideo = { [weak menuController, weak legacyController] overlayController, videoURL, previewImage, duration, dimensions, adjustments, caption, stickers, timer in + controller.finishedWithVideo = { [weak menuController, weak legacyController] overlayController, videoURL, previewImage, duration, dimensions, adjustments, caption, entities, stickers, timer in if let videoURL = videoURL { let description = NSMutableDictionary() description["type"] = "video" @@ -93,7 +103,7 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen if let timer = timer { description["timer"] = timer } - if let item = legacyAssetPickerItemGenerator()(description, caption, nil) { + if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil) { sendMessagesWithSignals([SSignal.single(item)]) } } diff --git a/TelegramUI/LegacyComponentsStickers.swift b/TelegramUI/LegacyComponentsStickers.swift index 1fbad493d7..1df3eddfab 100644 --- a/TelegramUI/LegacyComponentsStickers.swift +++ b/TelegramUI/LegacyComponentsStickers.swift @@ -47,7 +47,7 @@ func legacyComponentsStickers(postbox: Postbox, namespace: Int32) -> SSignal { let stickerPacks = NSMutableArray() for (id, info, _) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { - let pack = TGStickerPack(packReference: TGStickerPackIdReference(), title: info.title, stickerAssociations: [], documents: stickerPackDocuments[id] ?? [], packHash: info.hash, hidden: false, isMask: true)! + let pack = TGStickerPack(packReference: TGStickerPackIdReference(), title: info.title, stickerAssociations: [], documents: stickerPackDocuments[id] ?? [], packHash: info.hash, hidden: false, isMask: true, isFeatured: false, installedDate: 0)! stickerPacks.add(pack) } } diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 686d061fdd..f3edfaf9b9 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -166,18 +166,41 @@ final class LegacyControllerContext: NSObject, LegacyComponentsContext { } public func currentSizeClass() -> UIUserInterfaceSizeClass { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + if case .regular = validLayout.metrics.widthClass, case .regular = validLayout.metrics.heightClass { + return .regular + } + } return .compact } public func currentHorizontalSizeClass() -> UIUserInterfaceSizeClass { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + if case .regular = validLayout.metrics.widthClass { + return .regular + } + } return .compact } public func currentVerticalSizeClass() -> UIUserInterfaceSizeClass { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + if case .regular = validLayout.metrics.heightClass { + return .regular + } + } return .compact } public func sizeClassSignal() -> SSignal! { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + if case .regular = validLayout.metrics.heightClass { + return SSignal.single(UIUserInterfaceSizeClass.regular.rawValue as NSNumber) + } + } + if let controller = self.controller as? LegacyController, controller.enableSizeClassSignal { + //return controller.sizeClassSignal + } return SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber) } @@ -284,7 +307,14 @@ public class LegacyController: ViewController { var controllerLoaded: (() -> Void)? public var presentationCompleted: (() -> Void)? + private let sizeClass: SVariable = SVariable() + var enableSizeClassSignal: Bool = false + var sizeClassSignal: SSignal { + return self.sizeClass.signal()! + } + public init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, initialLayout: ContainerViewLayout? = nil) { + self.sizeClass.set(SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber)) self.presentation = presentation self.validLayout = initialLayout @@ -385,11 +415,26 @@ public class LegacyController: ViewController { } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let previousSizeClass: UIUserInterfaceSizeClass + if let validLayout = self.validLayout, case .regular = validLayout.metrics.widthClass { + previousSizeClass = .regular + } else { + previousSizeClass = .compact + } self.validLayout = layout super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + let updatedSizeClass: UIUserInterfaceSizeClass + if case .regular = layout.metrics.widthClass { + updatedSizeClass = .regular + } else { + updatedSizeClass = .compact + } + if previousSizeClass != updatedSizeClass { + self.sizeClass.set(SSignal.single(updatedSizeClass.rawValue as NSNumber)) + } } override open func dismiss(completion: (() -> Void)? = nil) { diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift index 08b8622086..0801d5e497 100644 --- a/TelegramUI/LegacyLocationController.swift +++ b/TelegramUI/LegacyLocationController.swift @@ -104,7 +104,7 @@ private func legacyRemainingTime(message: TGMessage) -> SSignal { }) } -func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, account: Account, openPeer: @escaping (Peer) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, stopLiveLocation: @escaping () -> Void) -> ViewController { +func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, account: Account, openPeer: @escaping (Peer) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, stopLiveLocation: @escaping () -> Void, shareLocation: @escaping (TelegramMediaMap) -> Void) -> ViewController { let legacyAuthor: AnyObject? = message.author.flatMap(makeLegacyPeer) let legacyLocation = TGLocationMediaAttachment() @@ -201,6 +201,15 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco controller.navigation_setDismiss({ [weak legacyController] in legacyController?.dismiss() }, rootController: nil) + controller.presentShareMenu = { menuController, coordinate in + menuController?.dismiss(animated: true) + if coordinate.latitude.isEqual(to: mapMedia.latitude) && coordinate.longitude.isEqual(to: mapMedia.longitude) { + shareLocation(mapMedia) + } else { + shareLocation(TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)) + } + return true + } /*controller.shareAction = { [weak legacyController] in if let legacyController = legacyController { var shareAction: (([PeerId]) -> Void)? diff --git a/TelegramUI/LegacyLocationPicker.swift b/TelegramUI/LegacyLocationPicker.swift index 38b712b016..0f4ad1e9da 100644 --- a/TelegramUI/LegacyLocationPicker.swift +++ b/TelegramUI/LegacyLocationPicker.swift @@ -3,12 +3,13 @@ import Display import LegacyComponents import TelegramCore import Postbox +import SwiftSignalKit private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) } -func legacyLocationPickerController(selfPeer: Peer, peer: Peer, sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, theme: PresentationTheme) -> ViewController { +func legacyLocationPickerController(account: Account, selfPeer: Peer, peer: Peer, sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, theme: PresentationTheme) -> ViewController { let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: theme) let controller = TGLocationPickerController(context: legacyController.context, intent: TGLocationPickerControllerDefaultIntent)! controller.peer = makeLegacyPeer(selfPeer) @@ -39,5 +40,50 @@ func legacyLocationPickerController(selfPeer: Peer, peer: Peer, sendLocation: @e sendLiveLocation(coordinate, period) legacyController?.dismiss() } + controller.nearbyPlacesSignal = { query, location in + return SSignal(generator: { subscriber in + let nearbyPlacesSignal: Signal<[TGLocationVenue], NoError> = resolvePeerByName(account: account, name: "foursquare") + |> take(1) + |> mapToSignal { peerId -> Signal in + guard let peerId = peerId else { + return .single(nil) + } + return requestChatContextResults(account: account, botId: peerId, peerId: selfPeer.id, query: query ?? "", location: .single((location?.coordinate.latitude ?? 0.0, location?.coordinate.longitude ?? 0.0)), offset: "") + } + |> mapToSignal { contextResult -> Signal<[TGLocationVenue], NoError> in + guard let contextResult = contextResult else { + return .single([]) + } + + var list: [TGLocationVenue] = [] + for result in contextResult.results { + switch result.message { + case let .mapLocation(mapMedia, _): + let legacyLocation = TGLocationMediaAttachment() + legacyLocation.latitude = mapMedia.latitude + legacyLocation.longitude = mapMedia.longitude + if let venue = mapMedia.venue { + legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id, type: venue.type) + } + list.append(TGLocationVenue(locationAttachment: legacyLocation)) + default: + break + } + } + + return .single(list) + } + + let disposable = nearbyPlacesSignal.start(next: { next in + subscriber?.putNext(next as NSArray) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + } return legacyController } diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index cc267686d3..eccb13fa4a 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -91,8 +91,8 @@ private final class LegacyAssetItemWrapper: NSObject { } } -func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashable : Any]?) { - return { anyDict, caption, hash in +func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [AnyHashable : Any]?) { + return { anyDict, caption, entities, hash in let dict = anyDict as! NSDictionary if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { let image = dict["image"] as! UIImage @@ -107,8 +107,16 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab } var result: [AnyHashable: Any] = [:] if asFile { - //result["item" as NSString] = LegacyAssetItemWrapper(item: .file(.asset(asset.backingAsset))) - return nil + var mimeType = "image/jpeg" + if let customMimeType = dict["mimeType"] as? String { + mimeType = customMimeType + } + var name = "image.jpg" + if let customName = dict["fileName"] as? String { + name = customName + } + + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), mimeType: mimeType, name: name, caption: caption), timer: nil, groupedId: nil) } else { result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) } @@ -138,13 +146,28 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab var result: [AnyHashable: Any] = [:] result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result - } else if let url = dict["url"] as? String { + } else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } + } else if (dict["type"] as! NSString) == "cameraVideo" { + var asFile = false + if let document = dict["document"] as? NSNumber, document.boolValue { + asFile = true + } + + let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString + + if let url = url, let previewImage = dict["previewImage"] as? UIImage { + let dimensions = previewImage.pixelSize() + let duration = (dict["duration"]! as AnyObject).doubleValue! + var result: [AnyHashable: Any] = [:] + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + return result + } } return nil } @@ -218,6 +241,12 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + case let .asset(asset): + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) default: break } diff --git a/TelegramUI/LegacySecureIdAttachmentMenu.swift b/TelegramUI/LegacySecureIdAttachmentMenu.swift new file mode 100644 index 0000000000..262bcbeccd --- /dev/null +++ b/TelegramUI/LegacySecureIdAttachmentMenu.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import LegacyComponents +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +enum SecureIdAttachmentMenuType { + case generic + case multiple + case selfie +} + +/* + @property (nonatomic, readonly) NSString *documentType; + @property (nonatomic, readonly) NSString *documentSubtype; + @property (nonatomic, readonly) NSString *issuingCountry; + @property (nonatomic, readonly) NSString *lastName; + @property (nonatomic, readonly) NSString *firstName; + @property (nonatomic, readonly) NSString *documentNumber; + @property (nonatomic, readonly) NSString *nationality; + @property (nonatomic, readonly) NSDate *birthDate; + @property (nonatomic, readonly) NSString *gender; + @property (nonatomic, readonly) NSDate *expiryDate; + @property (nonatomic, readonly) NSString *optional1; + @property (nonatomic, readonly) NSString *optional2; + + @property (nonatomic, readonly) NSString *mrz; + */ + +struct SecureIdRecognizedDocumentData { + let documentType: String? + let documentSubtype: String? + let issuingCountry: String? + let lastName: String? + let firstName: String? + let documentNumber: String? + let birthDate: Date? + let gender: String? + let expiryDate: Date? +} + +func presentLegacySecureIdAttachmentMenu(account: Account, present: @escaping (ViewController) -> Void, validLayout: ContainerViewLayout, type: SecureIdAttachmentMenuType, completion: @escaping ([TelegramMediaResource], SecureIdRecognizedDocumentData?) -> Void) { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: validLayout) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + present(legacyController) + + let mappedIntent: TGPassportAttachIntent + switch type { + case .generic: + mappedIntent = TGPassportAttachIntentDefault + case .multiple: + mappedIntent = TGPassportAttachIntentMultiple + case .selfie: + mappedIntent = TGPassportAttachIntentSelfie + } + + guard let attachMenu = TGPassportAttachMenu.present(with: legacyController.context, parentController: emptyController, menuController: nil, title: "", intent: mappedIntent, uploadAction: { signal, completed in + if let signal = signal { + completed?() + let _ = (processedLegacySecureIdAttachmentItems(postbox: account.postbox, signal: signal) + |> mapToSignal { resources -> Signal<([TelegramMediaResource], SecureIdRecognizedDocumentData?), NoError> in + if case .generic = type { + return recognizedResources(postbox: account.postbox, resources: resources) + |> map { data -> ([TelegramMediaResource], SecureIdRecognizedDocumentData?) in + return (resources, data) + } + } else { + return .single((resources, nil)) + } + } + |> deliverOnMainQueue).start(next: { resourcesAndData in + completion(resourcesAndData.0, resourcesAndData.1) + }) + } + }, sourceView: nil, sourceRect: nil, barButtonItem: nil) else { + legacyController.dismiss() + return + } + attachMenu.didDismiss = { [weak legacyController] _ in + legacyController?.dismiss() + } + attachMenu.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } +} + +private func processedLegacySecureIdAttachmentItems(postbox: Postbox, signal: SSignal) -> Signal<[TelegramMediaResource], NoError> { + let wrappedSignal = Signal<[TelegramMediaResource], NoError> { subscriber in + let disposable = signal.start(next: { next in + if let dict = next as? [String: Any], let image = dict["image"] as? UIImage { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpeg" + let scaledSize = image.size.aspectFitted(CGSize(width: 2048.0, height: 2048.0)) + if let scaledImage = TGScaleImageToPixelSize(image, scaledSize), let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.84) { + let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) + let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) + subscriber.putNext([resource]) + } else { + subscriber.putNext([]) + } + } else { + subscriber.putNext([]) + } + }, completed: { + subscriber.putCompletion() + }) + return ActionDisposable { + disposable?.dispose() + } + } + let collectedItems: Signal<[TelegramMediaResource], NoError> = wrappedSignal + |> reduceLeft(value: [] as [TelegramMediaResource], f: { (list: [TelegramMediaResource], rest: [TelegramMediaResource]) -> [TelegramMediaResource] in + var list = list + list.append(contentsOf: rest) + return list + }) + return collectedItems +} + +private func recognizedResources(postbox: Postbox, resources: [TelegramMediaResource]) -> Signal { + var signals: [Signal] = [] + for resource in resources { + let image = Signal { subscriber in + let fetch = postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let data = (postbox.mediaBox.resourceData(resource) + |> map { data -> UIImage? in + if data.complete { + return UIImage(contentsOfFile: data.path) + } + return nil + }).start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + }) + return ActionDisposable { + fetch.dispose() + data.dispose() + } + } + |> last + let recognized = image + |> mapToSignal { image -> Signal in + if let image = image { + return Signal { subscriber in + let disposable = TGPassportOCR.recognizeMRZ(in: image)?.start(next: { value in + if let value = value as? TGPassportMRZ { + var issuingCountry: String? = nil + if let issuingCountryValue = value.issuingCountry { + issuingCountry = countryCodeAlpha3ToAlpha2(issuingCountryValue) + } + subscriber.putNext(SecureIdRecognizedDocumentData(documentType: value.documentType, documentSubtype: value.documentSubtype, issuingCountry: issuingCountry, lastName: value.lastName, firstName: value.firstName, documentNumber: value.documentNumber, birthDate: value.birthDate, gender: value.gender, expiryDate: value.expiryDate)) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + }, completed: nil) + + return ActionDisposable { + disposable?.dispose() + } + } + } else { + return .single(nil) + } + } + signals.append(recognized) + } + return combineLatest(signals) + |> map { values -> SecureIdRecognizedDocumentData? in + for value in values { + if let value = value { + return value + } + } + return nil + } +} + +private struct IsoCountryInfo { + var name: String + var numeric: String + var alpha2: String + var alpha3: String + var calling: String + var currency: String + var continent: String +} + +private func countryCodeAlpha3ToAlpha2(_ code: String) -> String? { + for country in IsoCountries.allCountries { + if country.alpha3 == code { + return country.alpha2 + } + } + return nil +} + +private class IsoCountries { + class var allCountries: Array { + get { + return [ + IsoCountryInfo(name: "Afghanistan", numeric: "004", alpha2: "AF", alpha3: "AFG", calling: "+93", currency: "AFN", continent: "AS" ), + IsoCountryInfo(name: "Åland Islands", numeric: "248", alpha2: "AX", alpha3: "ALA", calling: "+358", currency: "FIM", continent: "EU" ), + IsoCountryInfo(name: "Albania", numeric: "008", alpha2: "AL", alpha3: "ALB", calling: "+355", currency: "ALL", continent: "EU" ), + IsoCountryInfo(name: "Algeria", numeric: "012", alpha2: "DZ", alpha3: "DZA", calling: "+213", currency: "DZD", continent: "AF" ), + IsoCountryInfo(name: "American Samoa", numeric: "016", alpha2: "AS", alpha3: "ASM", calling: "+684", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Andorra", numeric: "020", alpha2: "AD", alpha3: "AND", calling: "+376", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Angola", numeric: "024", alpha2: "AO", alpha3: "AGO", calling: "+244", currency: "AOA", continent: "AF" ), + IsoCountryInfo(name: "Anguilla", numeric: "660", alpha2: "AI", alpha3: "AIA", calling: "+264", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Antarctica", numeric: "010", alpha2: "AQ", alpha3: "ATA", calling: "+672", currency: "AUD", continent: "AN" ), + IsoCountryInfo(name: "Antigua and Barbuda", numeric: "028", alpha2: "AG", alpha3: "ATG", calling: "+268", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Argentina", numeric: "032", alpha2: "AR", alpha3: "ARG", calling: "+54", currency: "ARS", continent: "SA" ), + IsoCountryInfo(name: "Armenia", numeric: "051", alpha2: "AM", alpha3: "ARM", calling: "+374", currency: "AMD", continent: "AS" ), + IsoCountryInfo(name: "Aruba", numeric: "533", alpha2: "AW", alpha3: "ABW", calling: "+297", currency: "AWG", continent: "NA" ), + IsoCountryInfo(name: "Australia", numeric: "036", alpha2: "AU", alpha3: "AUS", calling: "+61", currency: "AUD", continent: "OC" ), + IsoCountryInfo(name: "Austria", numeric: "040", alpha2: "AT", alpha3: "AUT", calling: "+43", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Azerbaijan", numeric: "031", alpha2: "AZ", alpha3: "AZE", calling: "+994", currency: "AZN", continent: "AS" ), + IsoCountryInfo(name: "Bahamas", numeric: "044", alpha2: "BS", alpha3: "BHS", calling: "+242", currency: "BSD", continent: "NA" ), + IsoCountryInfo(name: "Bahrain", numeric: "048", alpha2: "BH", alpha3: "BHR", calling: "+973", currency: "BHD", continent: "AS" ), + IsoCountryInfo(name: "Bangladesh", numeric: "050", alpha2: "BD", alpha3: "BGD", calling: "+880", currency: "BDT", continent: "AS" ), + IsoCountryInfo(name: "Barbados", numeric: "052", alpha2: "BB", alpha3: "BRB", calling: "+246", currency: "BBD", continent: "NA" ), + IsoCountryInfo(name: "Belarus", numeric: "112", alpha2: "BY", alpha3: "BLR", calling: "+375", currency: "BYR", continent: "EU" ), + IsoCountryInfo(name: "Belgium", numeric: "056", alpha2: "BE", alpha3: "BEL", calling: "+32", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Belize", numeric: "084", alpha2: "BZ", alpha3: "BLZ", calling: "+501", currency: "BZD", continent: "NA" ), + IsoCountryInfo(name: "Benin", numeric: "204", alpha2: "BJ", alpha3: "BEN", calling: "+229", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Bermuda", numeric: "060", alpha2: "BM", alpha3: "BMU", calling: "+441", currency: "BMD", continent: "NA" ), + IsoCountryInfo(name: "Bhutan", numeric: "064", alpha2: "BT", alpha3: "BTN", calling: "+975", currency: "BTN", continent: "AS" ), + IsoCountryInfo(name: "Bolivia, Plurinational State of", numeric: "068", alpha2: "BO", alpha3: "BOL", calling: "+591", currency: "BOB", continent: "SA" ), + IsoCountryInfo(name: "Bonaire, Sint Eustatius and Saba", numeric: "535", alpha2: "BQ", alpha3: "BES", calling: "+599", currency: "USD", continent: "" ), + IsoCountryInfo(name: "Bosnia and Herzegovina", numeric: "070", alpha2: "BA", alpha3: "BIH", calling: "+387", currency: "BAM", continent: "EU" ), + IsoCountryInfo(name: "Botswana", numeric: "072", alpha2: "BW", alpha3: "BWA", calling: "+267", currency: "BWP", continent: "AF" ), + IsoCountryInfo(name: "Bouvet Island", numeric: "074", alpha2: "BV", alpha3: "BVT", calling: "+47", currency: "NOK", continent: "AN" ), + IsoCountryInfo(name: "Brazil", numeric: "076", alpha2: "BR", alpha3: "BRA", calling: "+55", currency: "BRL", continent: "SA" ), + IsoCountryInfo(name: "British Indian Ocean Territory", numeric: "086", alpha2: "IO", alpha3: "IOT", calling: "+246", currency: "USD", continent: "AS" ), + IsoCountryInfo(name: "Brunei Darussalam", numeric: "096", alpha2: "BN", alpha3: "BRN", calling: "+673", currency: "BND", continent: "AS" ), + IsoCountryInfo(name: "Bulgaria", numeric: "100", alpha2: "BG", alpha3: "BGR", calling: "+359", currency: "BGN", continent: "EU" ), + IsoCountryInfo(name: "Burkina Faso", numeric: "854", alpha2: "BF", alpha3: "BFA", calling: "+226", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Burundi", numeric: "108", alpha2: "BI", alpha3: "BDI", calling: "+257", currency: "BIF", continent: "AF" ), + IsoCountryInfo(name: "Cambodia", numeric: "116", alpha2: "KH", alpha3: "KHM", calling: "+855", currency: "KHR", continent: "AS" ), + IsoCountryInfo(name: "Cameroon", numeric: "120", alpha2: "CM", alpha3: "CMR", calling: "+237", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Canada", numeric: "124", alpha2: "CA", alpha3: "CAN", calling: "+1", currency: "CAD", continent: "NA" ), + IsoCountryInfo(name: "Cabo Verde", numeric: "132", alpha2: "CV", alpha3: "CPV", calling: "+238", currency: "CVE", continent: "AF" ), + IsoCountryInfo(name: "Cayman Islands", numeric: "136", alpha2: "KY", alpha3: "CYM", calling: "+345", currency: "KYD", continent: "NA" ), + IsoCountryInfo(name: "Central African Republic", numeric: "140", alpha2: "CF", alpha3: "CAF", calling: "+236", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Chad", numeric: "148", alpha2: "TD", alpha3: "TCD", calling: "+235", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Chile", numeric: "152", alpha2: "CL", alpha3: "CHL", calling: "+56", currency: "CLP", continent: "SA" ), + IsoCountryInfo(name: "China", numeric: "156", alpha2: "CN", alpha3: "CHN", calling: "+86", currency: "CNY", continent: "AS" ), + IsoCountryInfo(name: "Christmas Island", numeric: "162", alpha2: "CX", alpha3: "CXR", calling: "+61", currency: "AUD", continent: "AS" ), + IsoCountryInfo(name: "Cocos (Keeling) Islands", numeric: "166", alpha2: "CC", alpha3: "CCK", calling: "+891", currency: "AUD", continent: "AS" ), + IsoCountryInfo(name: "Colombia", numeric: "170", alpha2: "CO", alpha3: "COL", calling: "+57", currency: "COP", continent: "SA" ), + IsoCountryInfo(name: "Comoros", numeric: "174", alpha2: "KM", alpha3: "COM", calling: "+269", currency: "KMF", continent: "AF" ), + IsoCountryInfo(name: "Congo", numeric: "178", alpha2: "CG", alpha3: "COG", calling: "+242", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Congo, the Democratic Republic of the", numeric: "180", alpha2: "CD", alpha3: "COD", calling: "+243", currency: "CDF", continent: "AF" ), + IsoCountryInfo(name: "Cook Islands", numeric: "184", alpha2: "CK", alpha3: "COK", calling: "+682", currency: "NZD", continent: "OC" ), + IsoCountryInfo(name: "Costa Rica", numeric: "188", alpha2: "CR", alpha3: "CRI", calling: "+506", currency: "CRC", continent: "NA" ), + IsoCountryInfo(name: "Côte d'Ivoire", numeric: "384", alpha2: "CI", alpha3: "CIV", calling: "+225", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Croatia", numeric: "191", alpha2: "HR", alpha3: "HRV", calling: "+385", currency: "HRK", continent: "EU" ), + IsoCountryInfo(name: "Cuba", numeric: "192", alpha2: "CU", alpha3: "CUB", calling: "+53", currency: "CUP", continent: "NA" ), + IsoCountryInfo(name: "Curaçao", numeric: "531", alpha2: "CW", alpha3: "CUW", calling: "+599", currency: "ANG", continent: "" ), + IsoCountryInfo(name: "Cyprus", numeric: "196", alpha2: "CY", alpha3: "CYP", calling: "+357", currency: "EUR", continent: "AS" ), + IsoCountryInfo(name: "Czech Republic", numeric: "203", alpha2: "CZ", alpha3: "CZE", calling: "+420", currency: "CZK", continent: "EU" ), + IsoCountryInfo(name: "Denmark", numeric: "208", alpha2: "DK", alpha3: "DNK", calling: "+45", currency: "DKK", continent: "EU" ), + IsoCountryInfo(name: "Djibouti", numeric: "262", alpha2: "DJ", alpha3: "DJI", calling: "+253", currency: "DJF", continent: "AF" ), + IsoCountryInfo(name: "Dominica", numeric: "212", alpha2: "DM", alpha3: "DMA", calling: "+767", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Dominican Republic", numeric: "214", alpha2: "DO", alpha3: "DOM", calling: "+809", currency: "DOP", continent: "NA" ), + IsoCountryInfo(name: "Ecuador", numeric: "218", alpha2: "EC", alpha3: "ECU", calling: "+593", currency: "USD", continent: "SA" ), + IsoCountryInfo(name: "Egypt", numeric: "818", alpha2: "EG", alpha3: "EGY", calling: "+20", currency: "EGP", continent: "AF" ), + IsoCountryInfo(name: "El Salvador", numeric: "222", alpha2: "SV", alpha3: "SLV", calling: "+503", currency: "SVC", continent: "NA" ), + IsoCountryInfo(name: "Equatorial Guinea", numeric: "226", alpha2: "GQ", alpha3: "GNQ", calling: "+240", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Eritrea", numeric: "232", alpha2: "ER", alpha3: "ERI", calling: "+291", currency: "ETB", continent: "AF" ), + IsoCountryInfo(name: "Estonia", numeric: "233", alpha2: "EE", alpha3: "EST", calling: "+372", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Ethiopia", numeric: "231", alpha2: "ET", alpha3: "ETH", calling: "+251", currency: "ETB", continent: "AF" ), + IsoCountryInfo(name: "Falkland Islands (Malvinas)", numeric: "238", alpha2: "FK", alpha3: "FLK", calling: "+500", currency: "FKP", continent: "SA" ), + IsoCountryInfo(name: "Faroe Islands", numeric: "234", alpha2: "FO", alpha3: "FRO", calling: "+298", currency: "DKK", continent: "EU" ), + IsoCountryInfo(name: "Fiji", numeric: "242", alpha2: "FJ", alpha3: "FJI", calling: "+679", currency: "FJD", continent: "OC" ), + IsoCountryInfo(name: "Finland", numeric: "246", alpha2: "FI", alpha3: "FIN", calling: "+358", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "France", numeric: "250", alpha2: "FR", alpha3: "FRA", calling: "+33", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "French Guiana", numeric: "254", alpha2: "GF", alpha3: "GUF", calling: "+594", currency: "EUR", continent: "SA" ), + IsoCountryInfo(name: "French Polynesia", numeric: "258", alpha2: "PF", alpha3: "PYF", calling: "+689", currency: "XPF", continent: "OC" ), + IsoCountryInfo(name: "French Southern Territories", numeric: "260", alpha2: "TF", alpha3: "ATF", calling: "+689", currency: "EUR", continent: "AN" ), + IsoCountryInfo(name: "Gabon", numeric: "266", alpha2: "GA", alpha3: "GAB", calling: "+241", currency: "XAF", continent: "AF" ), + IsoCountryInfo(name: "Gambia", numeric: "270", alpha2: "GM", alpha3: "GMB", calling: "+220", currency: "GMD", continent: "AF" ), + IsoCountryInfo(name: "Georgia", numeric: "268", alpha2: "GE", alpha3: "GEO", calling: "+995", currency: "GEL", continent: "AS" ), + IsoCountryInfo(name: "Germany", numeric: "276", alpha2: "DE", alpha3: "DEU", calling: "+49", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Ghana", numeric: "288", alpha2: "GH", alpha3: "GHA", calling: "+233", currency: "GHS", continent: "AF" ), + IsoCountryInfo(name: "Gibraltar", numeric: "292", alpha2: "GI", alpha3: "GIB", calling: "+350", currency: "GIP", continent: "EU" ), + IsoCountryInfo(name: "Greece", numeric: "300", alpha2: "GR", alpha3: "GRC", calling: "+30", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Greenland", numeric: "304", alpha2: "GL", alpha3: "GRL", calling: "+299", currency: "DKK", continent: "NA" ), + IsoCountryInfo(name: "Grenada", numeric: "308", alpha2: "GD", alpha3: "GRD", calling: "+473", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Guadeloupe", numeric: "312", alpha2: "GP", alpha3: "GLP", calling: "+590", currency: "EUR", continent: "NA" ), + IsoCountryInfo(name: "Guam", numeric: "316", alpha2: "GU", alpha3: "GUM", calling: "+671", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Guatemala", numeric: "320", alpha2: "GT", alpha3: "GTM", calling: "+502", currency: "GTQ", continent: "NA" ), + IsoCountryInfo(name: "Guernsey", numeric: "831", alpha2: "GG", alpha3: "GGY", calling: "+1481", currency: "GGP", continent: "EU" ), + IsoCountryInfo(name: "Guinea", numeric: "324", alpha2: "GN", alpha3: "GIN", calling: "+225", currency: "GNF", continent: "AF" ), + IsoCountryInfo(name: "Guinea-Bissau", numeric: "624", alpha2: "GW", alpha3: "GNB", calling: "+245", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Guyana", numeric: "328", alpha2: "GY", alpha3: "GUY", calling: "+592", currency: "GYD", continent: "SA" ), + IsoCountryInfo(name: "Haiti", numeric: "332", alpha2: "HT", alpha3: "HTI", calling: "+509", currency: "HTG", continent: "NA" ), + IsoCountryInfo(name: "Heard Island and McDonald Islands", numeric: "334", alpha2: "HM", alpha3: "HMD", calling: "+61", currency: "AUD", continent: "AN" ), + IsoCountryInfo(name: "Holy See (Vatican City State)", numeric: "336", alpha2: "VA", alpha3: "VAT", calling: "+379", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Honduras", numeric: "340", alpha2: "HN", alpha3: "HND", calling: "+504", currency: "HNL", continent: "NA" ), + IsoCountryInfo(name: "Hong Kong", numeric: "344", alpha2: "HK", alpha3: "HKG", calling: "+852", currency: "HKD", continent: "AS" ), + IsoCountryInfo(name: "Hungary", numeric: "348", alpha2: "HU", alpha3: "HUN", calling: "+36", currency: "HUF", continent: "EU" ), + IsoCountryInfo(name: "Iceland", numeric: "352", alpha2: "IS", alpha3: "ISL", calling: "+354", currency: "ISK", continent: "EU" ), + IsoCountryInfo(name: "India", numeric: "356", alpha2: "IN", alpha3: "IND", calling: "+91", currency: "INR", continent: "AS" ), + IsoCountryInfo(name: "Indonesia", numeric: "360", alpha2: "ID", alpha3: "IDN", calling: "+62", currency: "IDR", continent: "AS" ), + IsoCountryInfo(name: "Iran, Islamic Republic of", numeric: "364", alpha2: "IR", alpha3: "IRN", calling: "+98", currency: "IRR", continent: "AS" ), + IsoCountryInfo(name: "Iraq", numeric: "368", alpha2: "IQ", alpha3: "IRQ", calling: "+964", currency: "IQD", continent: "AS" ), + IsoCountryInfo(name: "Ireland", numeric: "372", alpha2: "IE", alpha3: "IRL", calling: "+353", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Isle of Man", numeric: "833", alpha2: "IM", alpha3: "IMN", calling: "+44", currency: "IMP", continent: "EU" ), + IsoCountryInfo(name: "Israel", numeric: "376", alpha2: "IL", alpha3: "ISR", calling: "+972", currency: "ILS", continent: "AS" ), + IsoCountryInfo(name: "Italy", numeric: "380", alpha2: "IT", alpha3: "ITA", calling: "+39", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Jamaica", numeric: "388", alpha2: "JM", alpha3: "JAM", calling: "+876", currency: "JMD", continent: "NA" ), + IsoCountryInfo(name: "Japan", numeric: "392", alpha2: "JP", alpha3: "JPN", calling: "+81", currency: "JPY", continent: "AS" ), + IsoCountryInfo(name: "Jersey", numeric: "832", alpha2: "JE", alpha3: "JEY", calling: "+44", currency: "JEP", continent: "EU" ), + IsoCountryInfo(name: "Jordan", numeric: "400", alpha2: "JO", alpha3: "JOR", calling: "+962", currency: "JOD", continent: "AS" ), + IsoCountryInfo(name: "Kazakhstan", numeric: "398", alpha2: "KZ", alpha3: "KAZ", calling: "+7", currency: "KZT", continent: "AS" ), + IsoCountryInfo(name: "Kenya", numeric: "404", alpha2: "KE", alpha3: "KEN", calling: "+254", currency: "KES", continent: "AF" ), + IsoCountryInfo(name: "Kiribati", numeric: "296", alpha2: "KI", alpha3: "KIR", calling: "+686", currency: "AUD", continent: "OC" ), + IsoCountryInfo(name: "Korea, Democratic People's Republic of", numeric: "408", alpha2: "KP", alpha3: "PRK", calling: "+850", currency: "KPW", continent: "AS" ), + IsoCountryInfo(name: "Korea, Republic of", numeric: "410", alpha2: "KR", alpha3: "KOR", calling: "+82", currency: "KRW", continent: "AS" ), + IsoCountryInfo(name: "Kuwait", numeric: "414", alpha2: "KW", alpha3: "KWT", calling: "+965", currency: "KWD", continent: "AS" ), + IsoCountryInfo(name: "Kyrgyzstan", numeric: "417", alpha2: "KG", alpha3: "KGZ", calling: "+996", currency: "KGS", continent: "AS" ), + IsoCountryInfo(name: "Lao People's Democratic Republic", numeric: "418", alpha2: "LA", alpha3: "LAO", calling: "+856", currency: "LAK", continent: "AS" ), + IsoCountryInfo(name: "Latvia", numeric: "428", alpha2: "LV", alpha3: "LVA", calling: "+371", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Lebanon", numeric: "422", alpha2: "LB", alpha3: "LBN", calling: "+961", currency: "LBP", continent: "AS" ), + IsoCountryInfo(name: "Lesotho", numeric: "426", alpha2: "LS", alpha3: "LSO", calling: "+266", currency: "LSL", continent: "AF" ), + IsoCountryInfo(name: "Liberia", numeric: "430", alpha2: "LR", alpha3: "LBR", calling: "+231", currency: "LRD", continent: "AF" ), + IsoCountryInfo(name: "Libya", numeric: "434", alpha2: "LY", alpha3: "LBY", calling: "+218", currency: "LYD", continent: "AF" ), + IsoCountryInfo(name: "Liechtenstein", numeric: "438", alpha2: "LI", alpha3: "LIE", calling: "+423", currency: "CHF", continent: "EU" ), + IsoCountryInfo(name: "Lithuania", numeric: "440", alpha2: "LT", alpha3: "LTU", calling: "+370", currency: "LTL", continent: "EU" ), + IsoCountryInfo(name: "Luxembourg", numeric: "442", alpha2: "LU", alpha3: "LUX", calling: "+352", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Macao", numeric: "446", alpha2: "MO", alpha3: "MAC", calling: "+853", currency: "MOP", continent: "AS" ), + IsoCountryInfo(name: "Macedonia, the former Yugoslav Republic of", numeric: "807", alpha2: "MK", alpha3: "MKD", calling: "+389", currency: "MKD", continent: "EU" ), + IsoCountryInfo(name: "Madagascar", numeric: "450", alpha2: "MG", alpha3: "MDG", calling: "+261", currency: "MGA", continent: "AF" ), + IsoCountryInfo(name: "Malawi", numeric: "454", alpha2: "MW", alpha3: "MWI", calling: "+265", currency: "MWK", continent: "AF" ), + IsoCountryInfo(name: "Malaysia", numeric: "458", alpha2: "MY", alpha3: "MYS", calling: "+60", currency: "MYR", continent: "AS" ), + IsoCountryInfo(name: "Maldives", numeric: "462", alpha2: "MV", alpha3: "MDV", calling: "+960", currency: "MVR", continent: "AS" ), + IsoCountryInfo(name: "Mali", numeric: "466", alpha2: "ML", alpha3: "MLI", calling: "+223", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Malta", numeric: "470", alpha2: "MT", alpha3: "MLT", calling: "+356", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Marshall Islands", numeric: "584", alpha2: "MH", alpha3: "MHL", calling: "+692", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Martinique", numeric: "474", alpha2: "MQ", alpha3: "MTQ", calling: "+596", currency: "EUR", continent: "NA" ), + IsoCountryInfo(name: "Mauritania", numeric: "478", alpha2: "MR", alpha3: "MRT", calling: "+222", currency: "MRO", continent: "AF" ), + IsoCountryInfo(name: "Mauritius", numeric: "480", alpha2: "MU", alpha3: "MUS", calling: "+230", currency: "MUR", continent: "AF" ), + IsoCountryInfo(name: "Mayotte", numeric: "175", alpha2: "YT", alpha3: "MYT", calling: "+262", currency: "EUR", continent: "AF" ), + IsoCountryInfo(name: "Mexico", numeric: "484", alpha2: "MX", alpha3: "MEX", calling: "+52", currency: "MXN", continent: "NA" ), + IsoCountryInfo(name: "Micronesia, Federated States of", numeric: "583", alpha2: "FM", alpha3: "FSM", calling: "+691", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Moldova, Republic of", numeric: "498", alpha2: "MD", alpha3: "MDA", calling: "+373", currency: "MDL", continent: "EU" ), + IsoCountryInfo(name: "Monaco", numeric: "492", alpha2: "MC", alpha3: "MCO", calling: "+355", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Mongolia", numeric: "496", alpha2: "MN", alpha3: "MNG", calling: "+976", currency: "MNT", continent: "AS" ), + IsoCountryInfo(name: "Montenegro", numeric: "499", alpha2: "ME", alpha3: "MNE", calling: "+382", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Montserrat", numeric: "500", alpha2: "MS", alpha3: "MSR", calling: "+664", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Morocco", numeric: "504", alpha2: "MA", alpha3: "MAR", calling: "+212", currency: "MAD", continent: "AF" ), + IsoCountryInfo(name: "Mozambique", numeric: "508", alpha2: "MZ", alpha3: "MOZ", calling: "+258", currency: "MZN", continent: "AF" ), + IsoCountryInfo(name: "Myanmar", numeric: "104", alpha2: "MM", alpha3: "MMR", calling: "+95", currency: "MMK", continent: "AS" ), + IsoCountryInfo(name: "Namibia", numeric: "516", alpha2: "NA", alpha3: "NAM", calling: "+264", currency: "NAD", continent: "AF" ), + IsoCountryInfo(name: "Nauru", numeric: "520", alpha2: "NR", alpha3: "NRU", calling: "+674", currency: "AUD", continent: "OC" ), + IsoCountryInfo(name: "Nepal", numeric: "524", alpha2: "NP", alpha3: "NPL", calling: "+977", currency: "NPR", continent: "AS" ), + IsoCountryInfo(name: "Netherlands", numeric: "528", alpha2: "NL", alpha3: "NLD", calling: "+31", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "New Caledonia", numeric: "540", alpha2: "NC", alpha3: "NCL", calling: "+687", currency: "XPF", continent: "OC" ), + IsoCountryInfo(name: "New Zealand", numeric: "554", alpha2: "NZ", alpha3: "NZL", calling: "+64", currency: "NZD", continent: "OC" ), + IsoCountryInfo(name: "Nicaragua", numeric: "558", alpha2: "NI", alpha3: "NIC", calling: "+505", currency: "NIO", continent: "NA" ), + IsoCountryInfo(name: "Niger", numeric: "562", alpha2: "NE", alpha3: "NER", calling: "+277", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Nigeria", numeric: "566", alpha2: "NG", alpha3: "NGA", calling: "+234", currency: "NGN", continent: "AF" ), + IsoCountryInfo(name: "Niue", numeric: "570", alpha2: "NU", alpha3: "NIU", calling: "+683", currency: "NZD", continent: "OC" ), + IsoCountryInfo(name: "Norfolk Island", numeric: "574", alpha2: "NF", alpha3: "NFK", calling: "+672", currency: "AUD", continent: "OC" ), + IsoCountryInfo(name: "Northern Mariana Islands", numeric: "580", alpha2: "MP", alpha3: "MNP", calling: "+670", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Norway", numeric: "578", alpha2: "NO", alpha3: "NOR", calling: "+47", currency: "NOK", continent: "EU" ), + IsoCountryInfo(name: "Oman", numeric: "512", alpha2: "OM", alpha3: "OMN", calling: "+968", currency: "OMR", continent: "AS" ), + IsoCountryInfo(name: "Pakistan", numeric: "586", alpha2: "PK", alpha3: "PAK", calling: "+92", currency: "PKR", continent: "AS" ), + IsoCountryInfo(name: "Palau", numeric: "585", alpha2: "PW", alpha3: "PLW", calling: "+680", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Palestine, State of", numeric: "275", alpha2: "PS", alpha3: "PSE", calling: "+970", currency: "JOD", continent: "AS" ), + IsoCountryInfo(name: "Panama", numeric: "591", alpha2: "PA", alpha3: "PAN", calling: "+507", currency: "PAB", continent: "NA" ), + IsoCountryInfo(name: "Papua New Guinea", numeric: "598", alpha2: "PG", alpha3: "PNG", calling: "+675", currency: "PGK", continent: "OC" ), + IsoCountryInfo(name: "Paraguay", numeric: "600", alpha2: "PY", alpha3: "PRY", calling: "+595", currency: "PYG", continent: "SA" ), + IsoCountryInfo(name: "Peru", numeric: "604", alpha2: "PE", alpha3: "PER", calling: "+51", currency: "PEN", continent: "SA" ), + IsoCountryInfo(name: "Philippines", numeric: "608", alpha2: "PH", alpha3: "PHL", calling: "+63", currency: "PHP", continent: "AS" ), + IsoCountryInfo(name: "Pitcairn", numeric: "612", alpha2: "PN", alpha3: "PCN", calling: "+872", currency: "NZD", continent: "OC" ), + IsoCountryInfo(name: "Poland", numeric: "616", alpha2: "PL", alpha3: "POL", calling: "+48", currency: "PLN", continent: "EU" ), + IsoCountryInfo(name: "Portugal", numeric: "620", alpha2: "PT", alpha3: "PRT", calling: "+351", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Puerto Rico", numeric: "630", alpha2: "PR", alpha3: "PRI", calling: "+787", currency: "USD", continent: "NA" ), + IsoCountryInfo(name: "Qatar", numeric: "634", alpha2: "QA", alpha3: "QAT", calling: "+974", currency: "QAR", continent: "AS" ), + IsoCountryInfo(name: "Réunion", numeric: "638", alpha2: "RE", alpha3: "REU", calling: "+262", currency: "EUR", continent: "AF" ), + IsoCountryInfo(name: "Romania", numeric: "642", alpha2: "RO", alpha3: "ROU", calling: "+40", currency: "RON", continent: "EU" ), + IsoCountryInfo(name: "Russian Federation", numeric: "643", alpha2: "RU", alpha3: "RUS", calling: "+7", currency: "RUB", continent: "EU" ), + IsoCountryInfo(name: "Rwanda", numeric: "646", alpha2: "RW", alpha3: "RWA", calling: "+250", currency: "RWF", continent: "AF" ), + IsoCountryInfo(name: "Saint Barthélemy", numeric: "652", alpha2: "BL", alpha3: "BLM", calling: "+590", currency: "EUR", continent: "NA" ), + IsoCountryInfo(name: "Saint Helena, Ascension and Tristan da Cunha", numeric: "654", alpha2: "SH", alpha3: "SHN", calling: "+290", currency: "SHP", continent: "AF" ), + IsoCountryInfo(name: "Saint Kitts and Nevis", numeric: "659", alpha2: "KN", alpha3: "KNA", calling: "+869", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Saint Lucia", numeric: "662", alpha2: "LC", alpha3: "LCA", calling: "+758", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Saint Martin (French part)", numeric: "663", alpha2: "MF", alpha3: "MAF", calling: "+590", currency: "EUR", continent: "NA" ), + IsoCountryInfo(name: "Saint Pierre and Miquelon", numeric: "666", alpha2: "PM", alpha3: "SPM", calling: "+508", currency: "EUR", continent: "NA" ), + IsoCountryInfo(name: "Saint Vincent and the Grenadines", numeric: "670", alpha2: "VC", alpha3: "VCT", calling: "+784", currency: "XCD", continent: "NA" ), + IsoCountryInfo(name: "Samoa", numeric: "882", alpha2: "WS", alpha3: "WSM", calling: "+685", currency: "WST", continent: "OC" ), + IsoCountryInfo(name: "San Marino", numeric: "674", alpha2: "SM", alpha3: "SMR", calling: "+378", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Sao Tome and Principe", numeric: "678", alpha2: "ST", alpha3: "STP", calling: "+239", currency: "STD", continent: "AF" ), + IsoCountryInfo(name: "Saudi Arabia", numeric: "682", alpha2: "SA", alpha3: "SAU", calling: "+966", currency: "SAR", continent: "AS" ), + IsoCountryInfo(name: "Senegal", numeric: "686", alpha2: "SN", alpha3: "SEN", calling: "+221", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Serbia", numeric: "688", alpha2: "RS", alpha3: "SRB", calling: "+381", currency: "RSD", continent: "EU" ), + IsoCountryInfo(name: "Seychelles", numeric: "690", alpha2: "SC", alpha3: "SYC", calling: "+248", currency: "SCR", continent: "AF" ), + IsoCountryInfo(name: "Sierra Leone", numeric: "694", alpha2: "SL", alpha3: "SLE", calling: "+232", currency: "SLL", continent: "AF" ), + IsoCountryInfo(name: "Singapore", numeric: "702", alpha2: "SG", alpha3: "SGP", calling: "+65", currency: "SGD", continent: "AS" ), + IsoCountryInfo(name: "Sint Maarten (Dutch part)", numeric: "534", alpha2: "SX", alpha3: "SXM", calling: "+599", currency: "ANG", continent: "" ), + IsoCountryInfo(name: "Slovakia", numeric: "703", alpha2: "SK", alpha3: "SVK", calling: "+421", currency: "SKK", continent: "EU" ), + IsoCountryInfo(name: "Slovenia", numeric: "705", alpha2: "SI", alpha3: "SVN", calling: "+386", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Solomon Islands", numeric: "090", alpha2: "SB", alpha3: "SLB", calling: "+677", currency: "SBD", continent: "OC" ), + IsoCountryInfo(name: "Somalia", numeric: "706", alpha2: "SO", alpha3: "SOM", calling: "+252", currency: "SOS", continent: "AF" ), + IsoCountryInfo(name: "South Africa", numeric: "710", alpha2: "ZA", alpha3: "ZAF", calling: "+27", currency: "ZAR", continent: "AF" ), + IsoCountryInfo(name: "South Georgia and the South Sandwich Islands", numeric: "239", alpha2: "GS", alpha3: "SGS", calling: "+500", currency: "GBP", continent: "AN" ), + IsoCountryInfo(name: "South Sudan", numeric: "728", alpha2: "SS", alpha3: "SSD", calling: "+211", currency: "SSP", continent: "" ), + IsoCountryInfo(name: "Spain", numeric: "724", alpha2: "ES", alpha3: "ESP", calling: "+34", currency: "EUR", continent: "EU" ), + IsoCountryInfo(name: "Sri Lanka", numeric: "144", alpha2: "LK", alpha3: "LKA", calling: "+94", currency: "LKR", continent: "AS" ), + IsoCountryInfo(name: "Sudan", numeric: "729", alpha2: "SD", alpha3: "SDN", calling: "+249", currency: "SDG", continent: "AF" ), + IsoCountryInfo(name: "Suriname", numeric: "740", alpha2: "SR", alpha3: "SUR", calling: "+597", currency: "SRD", continent: "SA" ), + IsoCountryInfo(name: "Svalbard and Jan Mayen", numeric: "744", alpha2: "SJ", alpha3: "SJM", calling: "+47", currency: "NOK", continent: "EU" ), + IsoCountryInfo(name: "Swaziland", numeric: "748", alpha2: "SZ", alpha3: "SWZ", calling: "+268", currency: "CHF", continent: "AF" ), + IsoCountryInfo(name: "Sweden", numeric: "752", alpha2: "SE", alpha3: "SWE", calling: "+46", currency: "SEK", continent: "EU" ), + IsoCountryInfo(name: "Switzerland", numeric: "756", alpha2: "CH", alpha3: "CHE", calling: "+41", currency: "CHF", continent: "EU" ), + IsoCountryInfo(name: "Syrian Arab Republic", numeric: "760", alpha2: "SY", alpha3: "SYR", calling: "+963", currency: "SYP", continent: "AS" ), + IsoCountryInfo(name: "Taiwan, Province of China", numeric: "158", alpha2: "TW", alpha3: "TWN", calling: "+886", currency: "TWD", continent: "AS" ), + IsoCountryInfo(name: "Tajikistan", numeric: "762", alpha2: "TJ", alpha3: "TJK", calling: "+992", currency: "TJS", continent: "AS" ), + IsoCountryInfo(name: "Tanzania, United Republic of", numeric: "834", alpha2: "TZ", alpha3: "TZA", calling: "+255", currency: "TZS", continent: "AF" ), + IsoCountryInfo(name: "Thailand", numeric: "764", alpha2: "TH", alpha3: "THA", calling: "+66", currency: "THB", continent: "AS" ), + IsoCountryInfo(name: "Timor-Leste", numeric: "626", alpha2: "TL", alpha3: "TLS", calling: "+670", currency: "IDR", continent: "AS" ), + IsoCountryInfo(name: "Togo", numeric: "768", alpha2: "TG", alpha3: "TGO", calling: "+228", currency: "XOF", continent: "AF" ), + IsoCountryInfo(name: "Tokelau", numeric: "772", alpha2: "TK", alpha3: "TKL", calling: "+690", currency: "NZD", continent: "OC" ), + IsoCountryInfo(name: "Tonga", numeric: "776", alpha2: "TO", alpha3: "TON", calling: "+676", currency: "TOP", continent: "OC" ), + IsoCountryInfo(name: "Trinidad and Tobago", numeric: "780", alpha2: "TT", alpha3: "TTO", calling: "+868", currency: "TTD", continent: "NA" ), + IsoCountryInfo(name: "Tunisia", numeric: "788", alpha2: "TN", alpha3: "TUN", calling: "+216", currency: "TND", continent: "AF" ), + IsoCountryInfo(name: "Turkey", numeric: "792", alpha2: "TR", alpha3: "TUR", calling: "+90", currency: "TRY", continent: "EU" ), + IsoCountryInfo(name: "Turkmenistan", numeric: "795", alpha2: "TM", alpha3: "TKM", calling: "+993", currency: "TMM", continent: "AS" ), + IsoCountryInfo(name: "Turks and Caicos Islands", numeric: "796", alpha2: "TC", alpha3: "TCA", calling: "+649", currency: "USD", continent: "NA" ), + IsoCountryInfo(name: "Tuvalu", numeric: "798", alpha2: "TV", alpha3: "TUV", calling: "+688", currency: "TVD", continent: "OC" ), + IsoCountryInfo(name: "Uganda", numeric: "800", alpha2: "UG", alpha3: "UGA", calling: "+256", currency: "UGX", continent: "AF" ), + IsoCountryInfo(name: "Ukraine", numeric: "804", alpha2: "UA", alpha3: "UKR", calling: "+380", currency: "UAH", continent: "EU" ), + IsoCountryInfo(name: "United Arab Emirates", numeric: "784", alpha2: "AE", alpha3: "ARE", calling: "+971", currency: "AED", continent: "AS" ), + IsoCountryInfo(name: "United Kingdom", numeric: "826", alpha2: "GB", alpha3: "GBR", calling: "+44", currency: "GBP", continent: "EU" ), + IsoCountryInfo(name: "United States", numeric: "840", alpha2: "US", alpha3: "USA", calling: "+1", currency: "USD", continent: "NA" ), + IsoCountryInfo(name: "United States Minor Outlying Islands", numeric: "581", alpha2: "UM", alpha3: "UMI", calling: "+1", currency: "USD", continent: "OC" ), + IsoCountryInfo(name: "Uruguay", numeric: "858", alpha2: "UY", alpha3: "URY", calling: "+598", currency: "UYU", continent: "SA" ), + IsoCountryInfo(name: "Uzbekistan", numeric: "860", alpha2: "UZ", alpha3: "UZB", calling: "+998", currency: "UZS", continent: "AS" ), + IsoCountryInfo(name: "Vanuatu", numeric: "548", alpha2: "VU", alpha3: "VUT", calling: "+678", currency: "VUV", continent: "OC" ), + IsoCountryInfo(name: "Venezuela, Bolivarian Republic of", numeric: "862", alpha2: "VE", alpha3: "VEN", calling: "+58", currency: "VEF", continent: "SA" ), + IsoCountryInfo(name: "Vietnam", numeric: "704", alpha2: "VN", alpha3: "VNM", calling: "+84", currency: "VND", continent: "AS" ), + IsoCountryInfo(name: "Virgin Islands, British", numeric: "092", alpha2: "VG", alpha3: "VGB", calling: "+284", currency: "USD", continent: "NA" ), + IsoCountryInfo(name: "Virgin Islands, U.S.", numeric: "850", alpha2: "VI", alpha3: "VIR", calling: "+340", currency: "USD", continent: "NA" ), + IsoCountryInfo(name: "Wallis and Futuna", numeric: "876", alpha2: "WF", alpha3: "WLF", calling: "+681", currency: "XPF", continent: "OC" ), + IsoCountryInfo(name: "Western Sahara", numeric: "732", alpha2: "EH", alpha3: "ESH", calling: "+212", currency: "MAD", continent: "AF" ), + IsoCountryInfo(name: "Yemen", numeric: "887", alpha2: "YE", alpha3: "YEM", calling: "+967", currency: "YER", continent: "AS" ), + IsoCountryInfo(name: "Zambia", numeric: "894", alpha2: "ZM", alpha3: "ZMB", calling: "+260", currency: "ZMW", continent: "AF" ), + IsoCountryInfo(name: "Zimbabwe", numeric: "716", alpha2: "ZW", alpha3: "ZWE", calling: "+263", currency: "ZWD", continent: "AF" )] + } + } + +} diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index e19b31f4ad..9a6a7783ae 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -473,7 +473,9 @@ final class ListMessageSnippetItemNode: ListMessageNode { if case .longTap = gesture { item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url)) } else if url == self.currentPrimaryUrl { - item.controllerInteraction.openMessage(item.message) + if !item.controllerInteraction.openMessage(item.message) { + item.controllerInteraction.openUrl(url) + } } else { item.controllerInteraction.openUrl(url) } diff --git a/TelegramUI/LiveLocationManager.swift b/TelegramUI/LiveLocationManager.swift index b563adfc70..27603b31b3 100644 --- a/TelegramUI/LiveLocationManager.swift +++ b/TelegramUI/LiveLocationManager.swift @@ -230,9 +230,9 @@ public final class LiveLocationManager { let ids = self.broadcastToMessageIds.keys.filter({ $0.peerId == peerId }) if !ids.isEmpty { - let _ = self.postbox.modify({ modifier -> Void in + let _ = self.postbox.transaction({ transaction -> Void in for id in ids { - modifier.updateMessage(id, update: { currentMessage in + transaction.updateMessage(id, update: { currentMessage in var storeForwardInfo: StoreMessageForwardInfo? if let forwardInfo = currentMessage.forwardInfo { storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) diff --git a/TelegramUI/Markdown.swift b/TelegramUI/Markdown.swift index 6ee70b744b..e20d4f3590 100644 --- a/TelegramUI/Markdown.swift +++ b/TelegramUI/Markdown.swift @@ -79,7 +79,7 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt remainingRange = NSMakeRange(range.location + range.length, remainingRange.location + remainingRange.length - (range.location + range.length)) if let (parsedLinkText, parsedLinkContents) = parseLink(string: nsString, remainingRange: &remainingRange) { var linkAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: attributes.link.font, NSAttributedStringKey.foregroundColor: attributes.link.textColor, NSAttributedStringKey.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] - if !attributes.body.additionalAttributes.isEmpty { + if !attributes.link.additionalAttributes.isEmpty { for (key, value) in attributes.link.additionalAttributes { linkAttributes[NSAttributedStringKey(rawValue: key)] = value } diff --git a/TelegramUI/MediaInputPaneTrendingItem.swift b/TelegramUI/MediaInputPaneTrendingItem.swift index 326265fae2..a1c3d954ac 100644 --- a/TelegramUI/MediaInputPaneTrendingItem.swift +++ b/TelegramUI/MediaInputPaneTrendingItem.swift @@ -65,6 +65,25 @@ private let buttonFont = Font.medium(13.0) private final class TrendingTopItemNode: TransformImageNode { var file: TelegramMediaFile? = nil let loadDisposable = MetaDisposable() + + var currentIsPreviewing = false + + func updatePreviewing(animated: Bool, isPreviewing: Bool) { + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4, removeOnCompletion: false) + } + } else { + self.layer.removeAnimation(forKey: "transform.scale") + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + } + } + } } class MediaInputPaneTrendingItemNode: ListViewItemNode { @@ -226,7 +245,14 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode { } var offset: CGFloat = params.leftInset + leftInset - let itemSize = CGSize(width: 68.0, height: 68.0) + let availableWidth = params.width - params.leftInset - params.rightInset - leftInset * 2.0 + var itemSide: CGFloat = floor((availableWidth) / 5.0) + var itemSpacing: CGFloat = 0.0 + if itemSide >= 60.0 { + itemSpacing = max(0.0, min(6.0, itemSide - 60.0)) + } + itemSide = min(itemSide, 60.0) + let itemSize = CGSize(width: itemSide, height: itemSide) for i in 0 ..< topItems.count { let file = topItems[i].file @@ -248,7 +274,7 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode { let imageSize = dimensions.aspectFitted(itemSize) node.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0), size: imageSize) - offset += imageSize.width + 4.0 + offset += imageSize.width + itemSpacing } } @@ -258,6 +284,8 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode { strongSelf.itemNodes.remove(at: i) } } + + strongSelf.updatePreviewing(animated: false) } }) } @@ -284,4 +312,33 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode { } } } + + func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + guard let item = self.item else { + return nil + } + var index = 0 + for itemNode in self.itemNodes { + if itemNode.frame.contains(point), index < item.topItems.count { + return (itemNode, item.topItems[index]) + } + index += 1 + } + return nil + } + + func updatePreviewing(animated: Bool) { + guard let item = self.item else { + return + } + + var index = 0 + for itemNode in self.itemNodes { + if index < item.topItems.count { + let isPreviewing = item.interaction.getItemIsPreviewed(item.topItems[index]) + itemNode.updatePreviewing(animated: animated, isPreviewing: isPreviewing) + } + index += 1 + } + } } diff --git a/TelegramUI/MediaInputSettings.swift b/TelegramUI/MediaInputSettings.swift index 5f8ae2324f..78b5acb31d 100644 --- a/TelegramUI/MediaInputSettings.swift +++ b/TelegramUI/MediaInputSettings.swift @@ -39,8 +39,8 @@ public struct MediaInputSettings: PreferencesEntry, Equatable { } func updateMediaInputSettingsInteractively(postbox: Postbox, _ f: @escaping (MediaInputSettings) -> MediaInputSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings, { entry in let currentSettings: MediaInputSettings if let entry = entry as? MediaInputSettings { currentSettings = entry diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index a1e8830531..f292c6f07e 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -66,7 +66,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) + }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 46ea8acc50..f37a2607ed 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -649,11 +649,11 @@ private final class MediaPlayerContext { var reportRate = rate if loadedState.controlTimebase.isAudio { - if rate.isEqual(to: 1.0) { + if !rate.isZero { self.audioRenderer?.renderer.start() } self.audioRenderer?.renderer.setRate(rate) - if rate.isEqual(to: 1.0), let audioRenderer = self.audioRenderer { + if !rate.isZero, let audioRenderer = self.audioRenderer { let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase) if !timebaseRate.isEqual(to: rate) { reportRate = timebaseRate @@ -690,7 +690,7 @@ private final class MediaPlayerContext { whilePlaying = true } playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying) - } else if rate.isEqual(to: 1.0) { + } else if !rate.isZero { if reportRate.isZero { //playbackStatus = .buffering(initial: false, whilePlaying: true) playbackStatus = .playing diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index c260117600..2745ebce35 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -6,7 +6,7 @@ import TelegramCore private enum AudioPlayerRendererState { case paused - case playing(didSetRate: Bool) + case playing(rate: Double, didSetRate: Bool) } private final class AudioPlayerRendererBufferContext { @@ -82,20 +82,20 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U withPlayerRendererBuffer(Int32(intptr_t(bitPattern: refCon)), { context in context.with { context in switch context.state { - case let .playing(didSetRate): + case let .playing(rate, didSetRate): if context.buffer.availableBytes != 0 { let sampleIndex = context.bufferMaxChannelSampleIndex - Int64(context.buffer.availableBytes / (2 * 2)) if !didSetRate { - context.state = .playing(didSetRate: true) + context.state = .playing(rate: rate, didSetRate: true) let masterClock: CMClockOrTimebase if #available(iOS 9.0, *) { masterClock = CMTimebaseCopyMaster(context.timebase)! } else { masterClock = CMTimebaseGetMaster(context.timebase)! } - CMTimebaseSetRateAndAnchorTime(context.timebase, 1.0, CMTimeMake(sampleIndex, 44100), CMSyncGetTime(masterClock)) + CMTimebaseSetRateAndAnchorTime(context.timebase, rate, CMTimeMake(sampleIndex, 44100), CMSyncGetTime(masterClock)) updatedRate = context.updatedRate } else { context.renderTimestampTick += 1 @@ -249,18 +249,18 @@ private final class AudioPlayerRendererContext { self.closeAudioUnit() } - fileprivate func setPlaying(_ playing: Bool) { + fileprivate func setRate(_ rate: Double) { assert(audioPlayerRendererQueue.isCurrent()) - if playing && self.paused { + if !rate.isZero && self.paused { self.start() } self.bufferContext.with { context in - if playing { + if !rate.isZero { if case .playing = context.state { } else { - context.state = .playing(didSetRate: false) + context.state = .playing(rate: rate, didSetRate: false) } } else { context.state = .paused @@ -281,8 +281,8 @@ private final class AudioPlayerRendererContext { CMTimebaseSetTime(context.timebase, timestamp) switch context.state { - case .playing: - context.state = .playing(didSetRate: false) + case let .playing(rate, _): + context.state = .playing(rate: rate, didSetRate: false) case .paused: break } @@ -305,7 +305,7 @@ private final class AudioPlayerRendererContext { if !self.paused { self.paused = true - self.setPlaying(false) + self.setRate(0.0) self.closeAudioUnit() } } @@ -654,7 +654,7 @@ final class MediaPlayerAudioRenderer { audioPlayerRendererQueue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() - context.setPlaying(rate.isEqual(to: 1.0)) + context.setRate(rate) } } } diff --git a/TelegramUI/MessageContentKind.swift b/TelegramUI/MessageContentKind.swift index 0c8f2d2910..34c95c9ef0 100644 --- a/TelegramUI/MessageContentKind.swift +++ b/TelegramUI/MessageContentKind.swift @@ -15,6 +15,8 @@ enum MessageContentKind: Equatable { case game(String) case location case liveLocation + case expiredImage + case expiredVideo static func ==(lhs: MessageContentKind, rhs: MessageContentKind) -> Bool { switch lhs { @@ -90,6 +92,18 @@ enum MessageContentKind: Equatable { } else { return false } + case .expiredImage: + if case .expiredImage = rhs { + return true + } else { + return false + } + case .expiredVideo: + if case .expiredVideo = rhs { + return true + } else { + return false + } } } } @@ -97,6 +111,13 @@ enum MessageContentKind: Equatable { func messageContentKind(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> MessageContentKind { for media in message.media { switch media { + case let expiredMedia as TelegramMediaExpiredContent: + switch expiredMedia.data { + case .image: + return .expiredImage + case .file: + return .expiredVideo + } case _ as TelegramMediaImage: return .image case let file as TelegramMediaFile: @@ -190,5 +211,9 @@ func descriptionStringForMessage(_ message: Message, strings: PresentationString return (strings.Message_Location, true) case .liveLocation: return (strings.Message_LiveLocation, true) + case .expiredImage: + return (strings.Message_ImageExpired, true) + case .expiredVideo: + return (strings.Message_VideoExpired, true) } } diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 8d9cb11f49..99078dd91b 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -39,6 +39,12 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { private let account: Account private let trackingNode: MultiplexedVideoTrackingNode + var topInset: CGFloat = 0.0 { + didSet { + self.setNeedsLayout() + } + } + var bottomInset: CGFloat = 0.0 { didSet { self.setNeedsLayout() @@ -273,9 +279,9 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) var i = 0 - var offset = CGPoint(x: 0.0, y: 0.0) + var offset = CGPoint(x: 0.0, y: self.topInset) var previousItemSize: CGFloat = 0.0 - var contentMaxValueInScrollDirection: CGFloat = 0.0 + var contentMaxValueInScrollDirection: CGFloat = self.topInset let maxWidth = drawableSize.width let minimumInteritemSpacing: CGFloat = 1.0 diff --git a/TelegramUI/MusicPlaybackSettings.swift b/TelegramUI/MusicPlaybackSettings.swift index ebf99104d7..d2e27d20dc 100644 --- a/TelegramUI/MusicPlaybackSettings.swift +++ b/TelegramUI/MusicPlaybackSettings.swift @@ -59,8 +59,8 @@ public struct MusicPlaybackSettings: PreferencesEntry, Equatable { } func updateMusicPlaybackSettingsInteractively(postbox: Postbox, _ f: @escaping (MusicPlaybackSettings) -> MusicPlaybackSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings, { entry in let currentSettings: MusicPlaybackSettings if let entry = entry as? MusicPlaybackSettings { currentSettings = entry diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index 57204e4c11..685e8c0d63 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -30,7 +30,11 @@ public func navigateToChatController(navigationController: NavigationController, } else { controller = ChatController(account: account, chatLocation: chatLocation, messageId: messageId, botStart: botStart) } - navigationController.replaceAllButRootController(controller, animated: animated) + if account.telegramApplicationContext.immediateExperimentalUISettings.keepChatNavigationStack { + navigationController.pushViewController(controller) + } else { + navigationController.replaceAllButRootController(controller, animated: animated) + } } } diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index 85304908da..1c3e24e4ae 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -157,9 +157,9 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa var alignedTitleWidth = size.width - indicatorPadding var proxyPadding: CGFloat = 0.0 if !self.proxyNode.isHidden { - maxTitleWidth -= 20.0 + maxTitleWidth -= 25.0 alignedTitleWidth -= 20.0 - proxyPadding += 36.0 + proxyPadding += 39.0 } let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, maxTitleWidth), height: size.height)) @@ -172,7 +172,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa let titleFrame = titleContentRect self.titleNode.frame = titleFrame - let proxyFrame = CGRect(origin: CGPoint(x: clearBounds.maxX - 8.0 - self.proxyNode.bounds.width, y: 1.0 + floor((size.height - proxyNode.bounds.height) / 2.0)), size: proxyNode.bounds.size) + let proxyFrame = CGRect(origin: CGPoint(x: clearBounds.maxX - 16.0 - self.proxyNode.bounds.width, y: 1.0 + floor((size.height - proxyNode.bounds.height) / 2.0)), size: proxyNode.bounds.size) self.proxyNode.frame = proxyFrame self.proxyButton.frame = proxyFrame.insetBy(dx: -2.0, dy: -2.0) @@ -222,4 +222,11 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa func animateLayoutTransition() { } + + func proxyButtonRect() -> CGRect? { + if !self.proxyNode.isHidden { + return proxyNode.frame + } + return nil + } } diff --git a/TelegramUI/Notices.swift b/TelegramUI/Notices.swift index 34e26bc141..438b70c403 100644 --- a/TelegramUI/Notices.swift +++ b/TelegramUI/Notices.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import SwiftSignalKit -final class ApplicationSpecificBoolNotice: PostboxCoding { +final class ApplicationSpecificBoolNotice: NoticeEntry { init() { } @@ -11,6 +11,41 @@ final class ApplicationSpecificBoolNotice: PostboxCoding { func encode(_ encoder: PostboxEncoder) { } + + func isEqual(to: NoticeEntry) -> Bool { + if let _ = to as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } +} + +final class ApplicationSpecificVariantNotice: NoticeEntry { + let value: Bool + + init(value: Bool) { + self.value = value + } + + init(decoder: PostboxDecoder) { + self.value = decoder.decodeInt32ForKey("v", orElse: 0) != 0 + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.value ? 1 : 0, forKey: "v") + } + + func isEqual(to: NoticeEntry) -> Bool { + if let to = to as? ApplicationSpecificVariantNotice { + if self.value != to.value { + return false + } + return true + } else { + return false + } + } } private func noticeNamespace(namespace: Int32) -> ValueBoxKey { @@ -26,18 +61,43 @@ private func noticeKey(peerId: PeerId, key: Int32) -> ValueBoxKey { return v } +private enum ApplicationSpecificGlobalNotice: Int32 { + case secretChatInlineBotUsage = 0 + case secretChatLinkPreviews = 1 + case proxyAdsAcknowledgment = 2 + + var key: ValueBoxKey { + let v = ValueBoxKey(length: 4) + v.setInt32(0, value: self.rawValue) + return v + } +} + private struct ApplicationSpecificNoticeKeys { private static let botPaymentLiabilityNamespace: Int32 = 1 + private static let globalNamespace: Int32 = 2 static func botPaymentLiabilityNotice(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: botPaymentLiabilityNamespace), key: noticeKey(peerId: peerId, key: 0)) } + + static func secretChatInlineBotUsage() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.secretChatInlineBotUsage.key) + } + + static func secretChatLinkPreviews() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.secretChatLinkPreviews.key) + } + + static func proxyAdsAcknowledgment() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.proxyAdsAcknowledgment.key) + } } 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 postbox.transaction { transaction -> Bool in + if let _ = transaction.getNoticeEntry(key: ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId)) as? ApplicationSpecificBoolNotice { return true } else { return false @@ -46,9 +106,74 @@ struct ApplicationSpecificNotice { } static func setBotPaymentLiability(postbox: Postbox, peerId: PeerId) -> Signal { - return postbox.modify { modifier -> Void in - modifier.setNoticeEntry(key: ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId), value: ApplicationSpecificBoolNotice()) + return postbox.transaction { transaction -> Void in + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId), value: ApplicationSpecificBoolNotice()) + } + } + + static func getSecretChatInlineBotUsage(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Bool in + if let _ = transaction.getNoticeEntry(key: ApplicationSpecificNoticeKeys.secretChatInlineBotUsage()) as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } + } + + static func setSecretChatInlineBotUsage(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.secretChatInlineBotUsage(), value: ApplicationSpecificBoolNotice()) + } + } + + static func getSecretChatLinkPreviews(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Bool? in + if let value = transaction.getNoticeEntry(key: ApplicationSpecificNoticeKeys.secretChatLinkPreviews()) as? ApplicationSpecificVariantNotice { + return value.value + } else { + return nil + } + } + } + + static func getSecretChatLinkPreviews(_ entry: NoticeEntry) -> Bool? { + if let value = entry as? ApplicationSpecificVariantNotice { + return value.value + } else { + return nil + } + } + + static func setSecretChatLinkPreviews(postbox: Postbox, value: Bool) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.secretChatLinkPreviews(), value: ApplicationSpecificVariantNotice(value: value)) + } + } + + static func secretChatLinkPreviewsKey() -> NoticeEntryKey { + return ApplicationSpecificNoticeKeys.secretChatLinkPreviews() + } + + static func getProxyAdsAcknowledgment(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Bool in + if let _ = transaction.getNoticeEntry(key: ApplicationSpecificNoticeKeys.proxyAdsAcknowledgment()) as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } + } + + static func setProxyAdsAcknowledgment(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.proxyAdsAcknowledgment(), value: ApplicationSpecificBoolNotice()) + } + } + + static func reset(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + } } } - diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 56010bb8ce..52326a13fc 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -407,8 +407,8 @@ public func notificationsAndSoundsController(account: Account) -> ViewController ActionSheetButtonItem(title: presentationData.strings.Notifications_Reset, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - let modifyPeers = account.postbox.modify { modifier -> Void in - modifier.resetAllPeerNotificationSettings(TelegramPeerNotificationSettings.defaultSettings) + let modifyPeers = account.postbox.transaction { transaction -> Void in + transaction.resetAllPeerNotificationSettings(TelegramPeerNotificationSettings.defaultSettings) } let updateGlobal = updateGlobalNotificationSettingsInteractively(postbox: account.postbox, { _ in return GlobalNotificationSettingsSet.defaultSettings diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index 5dd67e5a2c..de75633c0b 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -78,7 +78,16 @@ final class OngoingCallContext { let queue = self.queue self.queue.async { - let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: proxyServer.flatMap { VoipProxyServer(host: $0.host, port: $0.port, username: $0.username, password: $0.password) }) + var voipProxyServer: VoipProxyServer? + if let proxyServer = proxyServer { + switch proxyServer.connection { + case let .socks5(username, password): + voipProxyServer = VoipProxyServer(host: proxyServer.host, port: proxyServer.port, username: username, password: password) + case .mtp: + break + } + } + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index 1fba90c4cc..06227b5b3f 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -240,17 +240,11 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat //endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, EP_TYPE_UDP_RELAY, peerTag)); } - voip_config_t config; - config.init_timeout = _callConnectTimeout; - config.recv_timeout = _callPacketTimeout; - config.data_saving = _dataSavingMode; - memset(config.logFilePath, 0, sizeof(config.logFilePath)); - config.enableAEC = false; - config.enableNS = true; - config.enableAGC = true; - memset(config.statsDumpFilePath, 0, sizeof(config.statsDumpFilePath)); + tgvoip::VoIPController::Config config(_callConnectTimeout, _callPacketTimeout, _dataSavingMode, false, true, true); + config.logFilePath = ""; + config.statsDumpFilePath = ""; - _controller->SetConfig(&config); + _controller->SetConfig(config); _controller->SetEncryptionKey((char *)key.bytes, isOutgoing); /*releasable*/ @@ -268,7 +262,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _controller->GetDebugLog(buffer); NSString *debugLog = [[NSString alloc] initWithUTF8String:buffer]; - voip_stats_t stats; + tgvoip::VoIPController::TrafficStats stats; _controller->GetStats(&stats); delete _controller; _controller = NULL; diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index b57f94c97b..8ab1da530e 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -192,10 +192,12 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever enqueueMessage(outMessage) }, stopLiveLocation: { account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: message.id.peerId) + }, shareLocation: { media in + present(ShareController(account: account, subject: .mapMedia(media), externalShare: true), nil) }), nil) return true case let .stickerPack(reference): - let controller = StickerPackPreviewController(account: account, stickerPack: reference) + let controller = StickerPackPreviewController(account: account, stickerPack: reference, parentNavigationController: navigationController) controller.sendSticker = sendSticker dismissInput() present(controller, nil) @@ -240,9 +242,9 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever return true case let .other(otherMedia): if let contact = otherMedia as? TelegramMediaContact { - let _ = (account.postbox.modify { modifier -> (Peer?, Bool?) in + let _ = (account.postbox.transaction { transaction -> (Peer?, Bool?) in if let peerId = contact.peerId { - return (modifier.getPeer(peerId), modifier.isPeerContact(peerId: peerId)) + return (transaction.getPeer(peerId), transaction.isPeerContact(peerId: peerId)) } else { return (nil, nil) } diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index 9750f1e47a..36623fad3d 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -16,15 +16,22 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon case let .channelMessage(peerId, messageId): openPeer(peerId, .chat(textInputState: nil, messageId: messageId)) case let .stickerPack(name): - present(StickerPackPreviewController(account: account, stickerPack: .name(name)), nil) + present(StickerPackPreviewController(account: account, stickerPack: .name(name), parentNavigationController: navigationController), nil) case let .instantView(webpage, anchor): navigationController?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) case let .join(link): present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in openPeer(peerId, .chat(textInputState: nil, messageId: nil)) }), nil) - case let .proxy(host, port, username, password): + case let .proxy(host, port, username, password, secret): let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - present(ProxyServerActionSheetController(account: account, theme: presentationData.theme, strings: presentationData.strings, server: ProxyServerSettings(host: host, port: port, username: username, password: password)), nil) + let server: ProxyServerSettings + if let secret = secret { + server = ProxyServerSettings(host: host, port: port, connection: .mtp(secret: secret)) + } else { + server = ProxyServerSettings(host: host, port: port, connection: .socks5(username: username, password: password)) + } + navigationController?.view.window?.endEditing(true) + present(ProxyServerActionSheetController(account: account, theme: presentationData.theme, strings: presentationData.strings, server: server), nil) } } diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 10ded0b2c0..6084eead00 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -17,19 +17,23 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre } else if let encoded = (url as NSString).addingPercentEscapes(using: String.Encoding.utf8.rawValue), let parsed = URL(string: encoded) { parsedUrlValue = parsed } + + if let parsedUrlValue = parsedUrlValue, parsedUrlValue.scheme == "mailto" { + applicationContext.applicationBindings.openUrl(url) + return + } + if let parsed = parsedUrlValue, parsed.scheme == nil { parsedUrlValue = URL(string: "https://" + parsed.absoluteString) } + if let parsed = parsedUrlValue, parsed.host == nil, let scheme = parsed.scheme, !scheme.isEmpty { + parsedUrlValue = URL(string: "https://" + parsed.absoluteString) + } guard let parsedUrl = parsedUrlValue else { return } - if parsedUrl.scheme == "mailto" { - applicationContext.applicationBindings.openUrl(url) - return - } - if let host = parsedUrl.host?.lowercased() { if host == "itunes.apple.com" { if applicationContext.applicationBindings.canOpenUrl(parsedUrl.absoluteString) { @@ -53,47 +57,7 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre let continueHandling: () -> Void = { if parsedUrl.scheme == "tg", let query = parsedUrl.query { var convertedUrl: String? - if parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var start: String? - var startGroup: String? - var game: String? - var post: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "start" { - start = value - } else if queryItem.name == "startgroup" { - startGroup = value - } else if queryItem.name == "game" { - game = value - } else if queryItem.name == "post" { - post = value - } - } - } - } - - if let domain = domain { - var result = "https://t.me/\(domain)" - if let post = post, let postValue = Int(post) { - result += "/\(postValue)" - } - if let start = start { - result += "?start=\(start)" - } else if let startGroup = startGroup { - result += "?startgroup=\(startGroup)" - } else if let game = game { - result += "?game=\(game)" - } - convertedUrl = result - } - } - } else if parsedUrl.host == "localpeer" { + if parsedUrl.host == "localpeer" { if let components = URLComponents(string: "/?" + query) { var peerId: PeerId? if let queryItems = components.queryItems { @@ -173,8 +137,8 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(shareUrl)")) } - let _ = (account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { @@ -192,12 +156,13 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre } } } - } else if parsedUrl.host == "socks" { + } else if parsedUrl.host == "socks" || parsedUrl.host == "proxy" { if let components = URLComponents(string: "/?" + query) { var server: String? var port: String? var user: String? var pass: String? + var secret: String? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -209,6 +174,8 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre user = value } else if queryItem.name == "pass" { pass = value + } else if queryItem.name == "secret" { + secret = value } } } @@ -222,20 +189,25 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre result += "&pass=\((pass as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" } } + if let secret = secret { + result += "&secret=\((secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } convertedUrl = result } } - } else if parsedUrl.host == "passport" { + } else if parsedUrl.host == "passport" || parsedUrl.host == "resolve" { if let components = URLComponents(string: "/?" + query) { + var domain: String? var botId: Int32? var scope: String? var publicKey: String? var opaquePayload = Data() - var errors: String? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "bot_id" { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "bot_id" { botId = Int32(value) } else if queryItem.name == "scope" { scope = value @@ -245,20 +217,76 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre if let data = value.data(using: .utf8) { opaquePayload = data } - } else if queryItem.name == "errors" { - errors = value } } } } - if let botId = botId, let scope = scope, let publicKey = publicKey { - let controller = SecureIdAuthController(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, opaquePayload: opaquePayload, errors: errors ?? "") - - if let navigationController = navigationController { - navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) - (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: nil) + let valid: Bool + if parsedUrl.host == "resolve" { + if domain == "telegrampassport" { + valid = true + } else { + valid = false } + } else { + valid = true + } + + if valid { + if let botId = botId, let scope = scope, let publicKey = publicKey { + let controller = SecureIdAuthController(account: account, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, opaquePayload: opaquePayload)) + + if let navigationController = navigationController { + navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) + + navigationController.view.window?.endEditing(true) + (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: nil) + } + } + return + } + } + } + + if parsedUrl.host == "resolve" { + if let components = URLComponents(string: "/?" + query) { + var domain: String? + var start: String? + var startGroup: String? + var game: String? + var post: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "start" { + start = value + } else if queryItem.name == "startgroup" { + startGroup = value + } else if queryItem.name == "game" { + game = value + } else if queryItem.name == "post" { + post = value + } + } + } + } + + if let domain = domain { + var result = "https://t.me/\(domain)" + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } + if let start = start { + result += "?start=\(start)" + } else if let startGroup = startGroup { + result += "?startgroup=\(startGroup)" + } else if let game = game { + result += "?game=\(game)" + } + convertedUrl = result } } } diff --git a/TelegramUI/OverlayPlayerController.swift b/TelegramUI/OverlayPlayerController.swift index 3e27cc3742..8e8f37fa24 100644 --- a/TelegramUI/OverlayPlayerController.swift +++ b/TelegramUI/OverlayPlayerController.swift @@ -43,8 +43,8 @@ final class OverlayPlayerController: ViewController { self?.dismiss() }, requestShare: { [weak self] messageId in if let strongSelf = self { - let _ = (strongSelf.account.postbox.modify { modifier -> Message? in - return modifier.getMessage(messageId) + let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) } |> deliverOnMainQueue).start(next: { message in if let strongSelf = self, let message = message { let shareController = ShareController(account: strongSelf.account, subject: .messages([message]), showInChat: { message in diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index defff8a2a8..48c52c2145 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -55,7 +55,9 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec return false } }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentController: { _, _ in }, navigationController: { + return nil + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index e0253c6310..20ac05826d 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -233,8 +233,8 @@ func passcodeOptionsController(account: Account) -> ViewController { let actionsDisposable = DisposableSet() let passcodeOptionsDataPromise = Promise() - passcodeOptionsDataPromise.set(combineLatest(account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() + passcodeOptionsDataPromise.set(combineLatest(account.postbox.transaction { transaction -> PostboxAccessChallengeData in + return transaction.getAccessChallengeData() }, account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.presentationPasscodeSettings]) |> take(1)) |> map { accessChallenge, preferences -> PasscodeOptionsData in return PasscodeOptionsData(accessChallenge: accessChallenge, presentationSettings: (preferences.values[ApplicationSpecificPreferencesKeys.presentationPasscodeSettings] as? PresentationPasscodeSettings) ?? PresentationPasscodeSettings.defaultSettings) }) @@ -248,9 +248,9 @@ func passcodeOptionsController(account: Account) -> ViewController { let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let challenge = PostboxAccessChallengeData.numericalPassword(value: result, timeout: nil, attempts: nil) - let _ = account.postbox.modify({ modifier -> Void in - modifier.setAccessChallengeData(challenge) - updatePresentationPasscodeSettingsInternal(modifier: modifier, { current in + let _ = account.postbox.transaction({ transaction -> Void in + transaction.setAccessChallengeData(challenge) + updatePresentationPasscodeSettingsInternal(transaction: transaction, { current in return current.withUpdatedAutolockTimeout(1 * 60 * 60) }) }).start() @@ -279,8 +279,8 @@ func passcodeOptionsController(account: Account) -> ViewController { actionSheet?.dismissAnimated() let challenge = PostboxAccessChallengeData.none - let _ = account.postbox.modify({ modifier -> Void in - modifier.setAccessChallengeData(challenge) + let _ = account.postbox.transaction({ transaction -> Void in + transaction.setAccessChallengeData(challenge) }).start() let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in @@ -301,10 +301,10 @@ func passcodeOptionsController(account: Account) -> ViewController { let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme) let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { - let _ = account.postbox.modify({ modifier -> Void in - var data = modifier.getAccessChallengeData() + let _ = account.postbox.transaction({ transaction -> Void in + var data = transaction.getAccessChallengeData() data = PostboxAccessChallengeData.numericalPassword(value: result, timeout: data.autolockDeadline, attempts: nil) - modifier.setAccessChallengeData(data) + transaction.setAccessChallengeData(data) }).start() let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in @@ -378,9 +378,9 @@ func passcodeOptionsController(account: Account) -> ViewController { let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: value ? TGPasscodeEntryControllerModeSetupSimple : TGPasscodeEntryControllerModeSetupComplex, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let challenge = value ? PostboxAccessChallengeData.numericalPassword(value: result, timeout: nil, attempts: nil) : PostboxAccessChallengeData.plaintextPassword(value: result, timeout: nil, attempts: nil) - let _ = account.postbox.modify({ modifier -> Void in - modifier.setAccessChallengeData(challenge) - updatePresentationPasscodeSettingsInternal(modifier: modifier, { current in + let _ = account.postbox.transaction({ transaction -> Void in + transaction.setAccessChallengeData(challenge) + updatePresentationPasscodeSettingsInternal(transaction: transaction, { current in return current.withUpdatedAutolockTimeout(1 * 60 * 60) }) }).start() @@ -425,8 +425,8 @@ func passcodeOptionsController(account: Account) -> ViewController { } public func passcodeOptionsAccessController(account: Account, animateIn: Bool = true, completion: @escaping (Bool) -> Void) -> Signal { - return account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() + return account.postbox.transaction { transaction -> PostboxAccessChallengeData in + return transaction.getAccessChallengeData() } |> deliverOnMainQueue |> map { challenge -> ViewController? in if case .none = challenge { @@ -470,12 +470,12 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = } } controller.updateAttemptData = { attemptData in - let _ = account.postbox.modify({ modifier -> Void in + let _ = account.postbox.transaction({ transaction -> Void in var attempts: AccessChallengeAttempts? if let attemptData = attemptData { attempts = AccessChallengeAttempts(count: Int32(attemptData.numberOfInvalidAttempts), timestamp: Int32(attemptData.dateOfLastInvalidAttempt)) } - var data = modifier.getAccessChallengeData() + var data = transaction.getAccessChallengeData() switch data { case .none: break @@ -484,7 +484,7 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = case let .plaintextPassword(value, timeout, _): data = .plaintextPassword(value: value, timeout: timeout, attempts: attempts) } - modifier.setAccessChallengeData(data) + transaction.setAccessChallengeData(data) }).start() } legacyController.bind(controller: controller) diff --git a/TelegramUI/PeerAvatarImageGalleryItem.swift b/TelegramUI/PeerAvatarImageGalleryItem.swift index ba11e17111..0c643249e8 100644 --- a/TelegramUI/PeerAvatarImageGalleryItem.swift +++ b/TelegramUI/PeerAvatarImageGalleryItem.swift @@ -5,15 +5,39 @@ import SwiftSignalKit import Postbox import TelegramCore +private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { + let account: Account + let representations: [TelegramMediaImageRepresentation] + + var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: self.representations, reference: nil) + if let representation = largestImageRepresentation(image.representations) { + return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions) + } else { + return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) + } + } + + func isEqual(to: GalleryThumbnailItem) -> Bool { + if let to = to as? PeerAvatarImageGalleryThumbnailItem { + return self.representations == to.representations + } else { + return false + } + } +} + class PeerAvatarImageGalleryItem: GalleryItem { let account: Account let strings: PresentationStrings let entry: AvatarGalleryEntry + let delete: (() -> Void)? - init(account: Account, strings: PresentationStrings, entry: AvatarGalleryEntry) { + init(account: Account, strings: PresentationStrings, entry: AvatarGalleryEntry, delete: (() -> Void)?) { self.account = account self.strings = strings self.entry = entry + self.delete = delete } func node() -> GalleryItemNode { @@ -24,6 +48,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { } node.setEntry(self.entry) + node.footerContentNode.delete = self.delete return node } @@ -35,11 +60,12 @@ class PeerAvatarImageGalleryItem: GalleryItem { } node.setEntry(self.entry) + node.footerContentNode.delete = self.delete } } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return nil + return (0, PeerAvatarImageGalleryThumbnailItem(account: self.account, representations: self.entry.representations)) } } @@ -53,7 +79,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _title = Promise() private let statusNodeContainer: HighlightableButtonNode private let statusNode: RadialStatusNode - //private let footerContentNode: ChatItemGalleryFooterContentNode + fileprivate let footerContentNode: AvatarGalleryItemFooterContentNode private let fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() @@ -63,7 +89,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.account = account self.imageNode = TransformImageNode() - //self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + self.footerContentNode = AvatarGalleryItemFooterContentNode(account: account) self.statusNodeContainer = HighlightableButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) @@ -83,6 +109,13 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) self.statusNodeContainer.isUserInteractionEnabled = false + + self.footerContentNode.share = { [weak self] interaction in + if let strongSelf = self, let entry = strongSelf.entry, !entry.representations.isEmpty { + let shareController = ShareController(account: strongSelf.account, subject: .image(entry.representations), saveToCameraRoll: true) + interaction.presentController(shareController, nil) + } + } } deinit { @@ -277,4 +310,8 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } } } + + override func footerContent() -> Signal { + return .single(self.footerContentNode) + } } diff --git a/TelegramUI/PeerBanTimeoutController.swift b/TelegramUI/PeerBanTimeoutController.swift new file mode 100644 index 0000000000..7535dc143f --- /dev/null +++ b/TelegramUI/PeerBanTimeoutController.swift @@ -0,0 +1,109 @@ +import Foundation +import Display +import AsyncDisplayKit +import UIKit +import SwiftSignalKit +import Photos + +final class PeerBanTimeoutController: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + init(theme: PresentationTheme, strings: PresentationStrings, currentValue: Int32, applyValue: @escaping (Int32?) -> Void) { + self.theme = theme + self.strings = strings + + super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + + self._ready.set(.single(true)) + + var updatedValue = currentValue + var items: [ActionSheetItem] = [] + items.append(PeerBanTimeoutActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in + updatedValue = value + })) + items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in + self?.dismissAnimated() + applyValue(updatedValue) + })) + 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 final class PeerBanTimeoutActionSheetItem: ActionSheetItem { + let strings: PresentationStrings + + let currentValue: Int32 + let valueChanged: (Int32) -> Void + + init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.strings = strings + self.currentValue = roundDateToDays(currentValue) + self.valueChanged = valueChanged + } + + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + return PeerBanTimeoutActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) + } + + func updateNode(_ node: ActionSheetItemNode) { + } +} + +private final class PeerBanTimeoutActionSheetItemNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + private let strings: PresentationStrings + + private let valueChanged: (Int32) -> Void + private let pickerView: UIDatePicker + + init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.valueChanged = valueChanged + + self.pickerView = UIDatePicker() + self.pickerView.datePickerMode = .date + self.pickerView.date = Date(timeIntervalSince1970: Double(roundDateToDays(currentValue))) + self.pickerView.locale = localeWithStrings(strings) + self.pickerView.minimumDate = Date() + self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) + + self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") + + super.init(theme: theme) + + self.view.addSubview(self.pickerView) + self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 216.0) + } + + override func layout() { + super.layout() + + self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.size.width, height: 216.0)) + } + + @objc private func datePickerUpdated() { + self.valueChanged(roundDateToDays(Int32(self.pickerView.date.timeIntervalSince1970))) + } +} diff --git a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift new file mode 100644 index 0000000000..1d8f21bf2b --- /dev/null +++ b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift @@ -0,0 +1,148 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +enum PeerChannelMemberContextKey: Hashable { + case recent + case recentSearch(String) + case admins + case restrictedAndBanned +} + +private final class PeerChannelMemberCategoriesContextsManagerImpl { + fileprivate var contexts: [PeerId: PeerChannelMemberCategoriesContext] = [:] + + func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + if let current = self.contexts[peerId] { + return current.getContext(key: key, updated: updated) + } else { + var becameEmptyImpl: ((Bool) -> Void)? + let context = PeerChannelMemberCategoriesContext(postbox: postbox, network: network, peerId: peerId, becameEmpty: { value in + becameEmptyImpl?(value) + }) + becameEmptyImpl = { [weak self, weak context] value in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self { + if let current = strongSelf.contexts[peerId], current === context { + strongSelf.contexts.removeValue(forKey: peerId) + } + } + } + self.contexts[peerId] = context + return context.getContext(key: key, updated: updated) + } + } + + func loadMore(peerId: PeerId, control: PeerChannelMemberCategoryControl) { + if let context = self.contexts[peerId] { + context.loadMore(control) + } + } +} + +final class PeerChannelMemberCategoriesContextsManager { + private let impl: QueueLocalObject + + init() { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return PeerChannelMemberCategoriesContextsManagerImpl() + }) + } + + func loadMore(peerId: PeerId, control: PeerChannelMemberCategoryControl?) { + if let control = control { + self.impl.with { impl in + impl.loadMore(peerId: peerId, control: control) + } + } + } + + private func getContext(postbox: Postbox, network: Network, peerId: PeerId, key: PeerChannelMemberContextKey, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + assert(Queue.mainQueue().isCurrent()) + if let (disposable, control) = self.impl.syncWith({ impl in + return impl.getContext(postbox: postbox, network: network, peerId: peerId, key: key, updated: updated) + }) { + return (disposable, control) + } else { + return (EmptyDisposable, nil) + } + } + + func recent(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + let key: PeerChannelMemberContextKey + if let searchQuery = searchQuery { + key = .recentSearch(searchQuery) + } else { + key = .recent + } + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: key, updated: updated) + } + + func admins(postbox: Postbox, network: Network, peerId: PeerId, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .admins, updated: updated) + } + + func restrictedAndBanned(postbox: Postbox, network: Network, peerId: PeerId, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(postbox: postbox, network: network, peerId: peerId, key: .restrictedAndBanned, updated: updated) + } + + func updateMemberBannedRights(account: Account, peerId: PeerId, memberId: PeerId, bannedRights: TelegramChannelBannedRights?) -> Signal { + return updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: bannedRights) + |> deliverOnMainQueue + |> beforeNext { [weak self] (previous, updated) in + if let strongSelf = self { + strongSelf.impl.with { impl in + for (_, context) in impl.contexts { + context.replayUpdates([(previous, updated)]) + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func updateMemberAdminRights(account: Account, peerId: PeerId, memberId: PeerId, adminRights: TelegramChannelAdminRights) -> Signal { + return updatePeerAdminRights(account: account, peerId: peerId, adminId: memberId, rights: adminRights) + |> map(Optional.init) + |> `catch` { _ -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, NoError> in + return .single(nil) + } + |> deliverOnMainQueue + |> beforeNext { [weak self] result in + if let strongSelf = self, let (previous, updated) = result { + strongSelf.impl.with { impl in + for (_, context) in impl.contexts { + context.replayUpdates([(previous, updated)]) + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func addMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { + return addChannelMember(account: account, peerId: peerId, memberId: memberId) + |> map(Optional.init) + |> `catch` { _ -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, NoError> in + return .single(nil) + } + |> deliverOnMainQueue + |> beforeNext { [weak self] result in + if let strongSelf = self, let (previous, updated) = result { + strongSelf.impl.with { impl in + for (_, context) in impl.contexts { + context.replayUpdates([(previous, updated)]) + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } +} diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index cecff4871f..30c22128fa 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -173,6 +173,8 @@ public class PeerMediaCollectionController: TelegramController { }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in + }, navigationController: { + return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] content in if let strongSelf = self { @@ -222,6 +224,7 @@ public class PeerMediaCollectionController: TelegramController { self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in + }, setupEditMessageMedia: { }, beginMessageSelection: { _ in }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { @@ -283,8 +286,8 @@ public class PeerMediaCollectionController: TelegramController { let controller = PeerSelectionController(account: strongSelf.account) controller.peerSelected = { [weak controller] peerId in if let strongSelf = self, let _ = controller { - let _ = (strongSelf.account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedForwardMessageIds(forwardMessageIds) } else { @@ -314,10 +317,10 @@ public class PeerMediaCollectionController: TelegramController { }, forwardMessages: { _ in }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { - let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in + let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in var messages: [Message] = [] for id in selectedIds { - if let message = modifier.getMessage(id) { + if let message = transaction.getMessage(id) { messages.append(message) } } @@ -357,6 +360,7 @@ public class PeerMediaCollectionController: TelegramController { }, lockMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { + }, displayRestrictedInfo: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift index a40695aac7..09376e42a5 100644 --- a/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -371,9 +371,9 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { case .regular, .reversed: inputIndex = .single(index) case .random: - inputIndex = self.postbox.modify { modifier -> MessageIndex in + inputIndex = self.postbox.transaction { transaction -> MessageIndex in - return modifier.findRandomMessage(peerId: peerId, tagMask: tagMask, ignoreId: index.id) ?? index + return transaction.findRandomMessage(peerId: peerId, tagMask: tagMask, ignoreId: index.id) ?? index } } let historySignal = inputIndex |> mapToSignal { inputIndex -> Signal in diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 3cbe60e02e..2e2861640d 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -11,6 +11,23 @@ public final class PeerSelectionController: ViewController { private var presentationDataDisposable: Disposable? var peerSelected: ((PeerId) -> Void)? + private let filter: ChatListNodePeersFilter + + var inProgress: Bool = false { + didSet { + if self.inProgress != oldValue { + if self.isNodeLoaded { + self.peerSelectionNode.inProgress = self.inProgress + } + + if self.inProgress { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme)) + } else { + self.navigationItem.rightBarButtonItem = nil + } + } + } + } private var peerSelectionNode: PeerSelectionControllerNode { return super.displayNode as! PeerSelectionControllerNode @@ -23,8 +40,9 @@ public final class PeerSelectionController: ViewController { return self._ready } - public init(account: Account) { + public init(account: Account, filter: ChatListNodePeersFilter = [.onlyWriteable]) { self.account = account + self.filter = filter self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -51,7 +69,7 @@ public final class PeerSelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerSelectionControllerNode(account: self.account, dismiss: { [weak self] in + self.displayNode = PeerSelectionControllerNode(account: self.account, filter: self.filter, dismiss: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) self.displayNode.backgroundColor = .white @@ -74,9 +92,9 @@ public final class PeerSelectionController: ViewController { self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peer in if let strongSelf = self { - let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in + let storedPeer = strongSelf.account.postbox.transaction { transaction -> Void in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in return updatedPeer }) } diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index 535b84f8d1..846d8b0c30 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -8,6 +8,13 @@ import SwiftSignalKit final class PeerSelectionControllerNode: ASDisplayNode { private let account: Account private let dismiss: () -> Void + private let filter: ChatListNodePeersFilter + + var inProgress: Bool = false { + didSet { + + } + } var navigationBar: NavigationBar? @@ -38,9 +45,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { return self.readyValue.get() } - init(account: Account, dismiss: @escaping () -> Void) { + init(account: Account, filter: ChatListNodePeersFilter, dismiss: @escaping () -> Void) { self.account = account self.dismiss = dismiss + self.filter = filter self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -54,7 +62,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.segmentedControl.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor self.segmentedControl.selectedSegmentIndex = 0 - self.chatListNode = ChatListNode(account: account, groupId: nil, controlsHistoryPreload: false, mode: .peers(onlyWriteable: true), theme: presentationData.theme, strings: presentationData.strings, timeFormat: presentationData.timeFormat) + self.chatListNode = ChatListNode(account: account, groupId: nil, controlsHistoryPreload: false, mode: .peers(filter: filter), theme: presentationData.theme, strings: presentationData.strings, timeFormat: presentationData.timeFormat) super.init() @@ -183,7 +191,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatListSearchContainerNode(account: self.account, onlyWriteable: true, groupId: nil, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatListSearchContainerNode(account: self.account, filter: self.filter, groupId: nil, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } @@ -218,8 +226,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let placeholderNode = maybePlaceholderNode { self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: true, openPeer: { [weak self] peerId in if let strongSelf = self { - let _ = (strongSelf.account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) + let _ = (strongSelf.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { strongSelf.requestOpenPeerFromSearch?(peer) @@ -268,7 +276,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { func scrollToTop() { if self.chatListNode.supernode != nil { - self.chatListNode.scrollToLatest() + self.chatListNode.scrollToPosition(.top) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.scrollToTop() } diff --git a/TelegramUI/PeerTitle.swift b/TelegramUI/PeerTitle.swift new file mode 100644 index 0000000000..6899244da0 --- /dev/null +++ b/TelegramUI/PeerTitle.swift @@ -0,0 +1,32 @@ +import Foundation +import TelegramCore +import Postbox + +extension Peer { + func displayTitle(strings: PresentationStrings) -> String { + switch self { + case let user as TelegramUser: + if let firstName = user.firstName { + if let lastName = user.lastName { + if strings.lc == 0x6b6f { + return "\(lastName) \(firstName)" + } else { + return "\(firstName) \(lastName)" + } + } else { + return firstName + } + } else if let lastName = user.lastName { + return lastName + } else { + return strings.User_DeletedAccount + } + case let group as TelegramGroup: + return group.title + case let channel as TelegramChannel: + return channel.title + default: + return "" + } + } +} diff --git a/TelegramUI/PhoneInputNode.swift b/TelegramUI/PhoneInputNode.swift index 4c3407b289..1f4a9d6da8 100644 --- a/TelegramUI/PhoneInputNode.swift +++ b/TelegramUI/PhoneInputNode.swift @@ -130,6 +130,8 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { var countryCodeTextUpdated: ((String) -> Void)? var numberTextUpdated: ((String) -> Void)? + var returnAction: (() -> Void)? + private let phoneFormatter = InteractivePhoneFormatter() private let fontSize: CGFloat @@ -173,6 +175,13 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { return self.enableEditing } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if textField == self.numberField.textField { + self.returnAction?() + } + return false + } + private func updateNumberFromTextFields() { let inputText = removeDuplicatedPlus(cleanPhoneNumber(self.countryCodeField.textField.text) + cleanPhoneNumber(self.numberField.textField.text)) self.updateNumber(inputText) @@ -182,7 +191,7 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { let (regionPrefix, text) = self.phoneFormatter.updateText(inputText) var realRegionPrefix: String let numberText: String - if let regionPrefix = regionPrefix, !regionPrefix.isEmpty { + if let regionPrefix = regionPrefix, !regionPrefix.isEmpty, regionPrefix != "+" { realRegionPrefix = cleanSuffix(regionPrefix) if !realRegionPrefix.hasPrefix("+") { realRegionPrefix = "+" + realRegionPrefix @@ -208,7 +217,36 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { self.countryCodeTextUpdated?(realRegionPrefix) if numberText != self.numberField.textField.text { + var restorePosition: Int? + if let text = self.numberField.textField.text, let selectedTextRange = self.numberField.textField.selectedTextRange { + let initialOffset = self.numberField.textField.offset(from: self.numberField.textField.beginningOfDocument, to: selectedTextRange.start) + var significantIndex = 0 + for i in 0 ..< min(initialOffset, text.count) { + let unicodeScalars = String(text[text.index(text.startIndex, offsetBy: i)]).unicodeScalars + if unicodeScalars.count == 1 && CharacterSet.decimalDigits.contains(unicodeScalars[unicodeScalars.startIndex]) { + significantIndex += 1 + } + } + var restoreIndex = 0 + for i in 0 ..< numberText.count { + if significantIndex <= 0 { + break + } + let unicodeScalars = String(numberText[numberText.index(numberText.startIndex, offsetBy: i)]).unicodeScalars + if unicodeScalars.count == 1 && CharacterSet.decimalDigits.contains(unicodeScalars[unicodeScalars.startIndex]) { + significantIndex -= 1 + } + restoreIndex += 1 + } + restorePosition = restoreIndex + } self.numberField.textField.text = numberText + if let restorePosition = restorePosition { + if let startPosition = self.numberField.textField.position(from: self.numberField.textField.beginningOfDocument, offset: restorePosition) { + let selectionRange = self.numberField.textField.textRange(from: startPosition, to: startPosition) + self.numberField.textField.selectedTextRange = selectionRange + } + } } self.numberTextUpdated?(numberText) diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index 2d1a081369..278f49da8c 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -14,6 +14,8 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case experimentalSettings = 8 case musicPlaybackSettings = 9 case mediaInputSettings = 10 + case experimentalUISettings = 11 + case contactSynchronizationSettings = 12 } public struct ApplicationSpecificPreferencesKeys { @@ -28,4 +30,6 @@ public struct ApplicationSpecificPreferencesKeys { public static let experimentalSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.experimentalSettings.rawValue) public static let musicPlaybackSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.musicPlaybackSettings.rawValue) public static let mediaInputSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.mediaInputSettings.rawValue) + public static let experimentalUISettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.experimentalUISettings.rawValue) + public static let contactSynchronizationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.contactSynchronizationSettings.rawValue) } diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift index be87c1f5e5..8f3ba59a64 100644 --- a/TelegramUI/PresentationCallManager.swift +++ b/TelegramUI/PresentationCallManager.swift @@ -83,10 +83,10 @@ public final class PresentationCallManager { if ringingStates.isEmpty { return .single([]) } else { - return postbox.modify { modifier -> [(Peer, CallSessionRingingState)] in + return postbox.transaction { transaction -> [(Peer, CallSessionRingingState)] in var result: [(Peer, CallSessionRingingState)] = [] for state in ringingStates { - if let peer = modifier.getPeer(state.peerId) { + if let peer = transaction.getPeer(state.peerId) { result.append((peer, state)) } } diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 72e006b8c6..0877cefb05 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -71,59 +71,81 @@ private func currentTimeFormat() -> PresentationTimeFormat { } } -public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings), NoError> { - return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings) in +public final class InitialPresentationDataAndSettings { + public let presentationData: PresentationData + public let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + public let loggingSettings: LoggingSettings + public let callListSettings: CallListSettings + public let inAppNotificationSettings: InAppNotificationSettings + public let mediaInputSettings: MediaInputSettings + public let experimentalUISettings: ExperimentalUISettings + + init(presentationData: PresentationData, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, loggingSettings: LoggingSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, experimentalUISettings: ExperimentalUISettings) { + self.presentationData = presentationData + self.automaticMediaDownloadSettings = automaticMediaDownloadSettings + self.loggingSettings = loggingSettings + self.callListSettings = callListSettings + self.inAppNotificationSettings = inAppNotificationSettings + self.mediaInputSettings = mediaInputSettings + self.experimentalUISettings = experimentalUISettings + } +} + +public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings, ExperimentalUISettings) in let themeSettings: PresentationThemeSettings - if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { + if let current = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current } else { themeSettings = PresentationThemeSettings.defaultSettings } let localizationSettings: LocalizationSettings? - if let current = modifier.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { + if let current = transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { localizationSettings = current } else { localizationSettings = nil } let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings) as? AutomaticMediaDownloadSettings { + if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings) as? AutomaticMediaDownloadSettings { automaticMediaDownloadSettings = value } else { automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings } let loggingSettings: LoggingSettings - if let value = modifier.getPreferencesEntry(key: PreferencesKeys.loggingSettings) as? LoggingSettings { + if let value = transaction.getPreferencesEntry(key: PreferencesKeys.loggingSettings) as? LoggingSettings { loggingSettings = value } else { loggingSettings = LoggingSettings.defaultSettings } let callListSettings: CallListSettings - if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings) as? CallListSettings { + if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings) as? CallListSettings { callListSettings = value } else { callListSettings = CallListSettings.defaultSettings } let inAppNotificationSettings: InAppNotificationSettings - if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings { + if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings { inAppNotificationSettings = value } else { inAppNotificationSettings = InAppNotificationSettings.defaultSettings } let mediaInputSettings: MediaInputSettings - if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings) as? MediaInputSettings { + if let value = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings) as? MediaInputSettings { mediaInputSettings = value } else { mediaInputSettings = MediaInputSettings.defaultSettings } - return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) - } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings) in + let experimentalUISettings: ExperimentalUISettings = (transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.experimentalUISettings) as? ExperimentalUISettings) ?? ExperimentalUISettings.defaultSettings + + return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings, experimentalUISettings) + } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings, experimentalUISettings) -> InitialPresentationDataAndSettings in let themeValue: PresentationTheme switch themeSettings.theme { case let .builtin(reference): @@ -145,7 +167,7 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres stringsValue = defaultPresentationStrings } let timeFormat: PresentationTimeFormat = currentTimeFormat() - return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) + return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings: automaticMediaDownloadSettings, loggingSettings: loggingSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, experimentalUISettings: experimentalUISettings) } } diff --git a/TelegramUI/PresentationPasscodeSettings.swift b/TelegramUI/PresentationPasscodeSettings.swift index c07065d197..b2fdd127ce 100644 --- a/TelegramUI/PresentationPasscodeSettings.swift +++ b/TelegramUI/PresentationPasscodeSettings.swift @@ -51,13 +51,13 @@ public struct PresentationPasscodeSettings: PreferencesEntry, Equatable { } func updatePresentationPasscodeSettingsInteractively(postbox: Postbox, _ f: @escaping (PresentationPasscodeSettings) -> PresentationPasscodeSettings) -> Signal { - return postbox.modify { modifier -> Void in - updatePresentationPasscodeSettingsInternal(modifier: modifier, f) + return postbox.transaction { transaction -> Void in + updatePresentationPasscodeSettingsInternal(transaction: transaction, f) } } -func updatePresentationPasscodeSettingsInternal(modifier: Modifier, _ f: @escaping (PresentationPasscodeSettings) -> PresentationPasscodeSettings) { - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationPasscodeSettings, { entry in +func updatePresentationPasscodeSettingsInternal(transaction: Transaction, _ f: @escaping (PresentationPasscodeSettings) -> PresentationPasscodeSettings) { + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationPasscodeSettings, { entry in let currentSettings: PresentationPasscodeSettings if let entry = entry as? PresentationPasscodeSettings { currentSettings = entry diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 5b43346989..acf8d172dc 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -327,44 +327,22 @@ struct PresentationResourcesChat { static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in - return generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - let diameter: CGFloat = 22.0 - context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.translateBy(x: 1.5, y: 2.5) - context.move(to: CGPoint(x: 11.0, y: 5.5)) - context.addLine(to: CGPoint(x: 11.0, y: 11.0)) - context.addLine(to: CGPoint(x: 14.5, y: 14.5)) - context.strokePath() + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/RecentTabIcon"), color: theme.chat.inputMediaPanel.panelIconColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } }) }) } static func chatInputMediaPanelRecentGifsIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelRecentGifsIconImage.rawValue, { theme in - return generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - let diameter: CGFloat = 22.0 - context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) - UIGraphicsPushContext(context) - - context.setTextDrawingMode(.stroke) - context.setLineWidth(0.65) - - ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSAttributedStringKey.font: Font.regular(8.0), NSAttributedStringKey.foregroundColor: theme.chat.inputMediaPanel.panelIconColor]) - - context.setTextDrawingMode(.fill) - context.setLineWidth(0.8) - - ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSAttributedStringKey.font: Font.regular(8.0), NSAttributedStringKey.foregroundColor: theme.chat.inputMediaPanel.panelIconColor]) - UIGraphicsPopContext() + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsTabIcon"), color: theme.chat.inputMediaPanel.panelIconColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } }) }) } diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index efd5b909f4..d154807223 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -93,7 +93,7 @@ func formatWithArgumentRanges(_ value: String, _ ranges: [(Int, NSRange)], _ arg return (result as String, resultingRanges) } public final class PresentationStrings { - private let lc: UInt32 + public let lc: UInt32 public let languageCode: String public let dict: [String: String] @@ -141,6 +141,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Call_StatusOngoing, self._Call_StatusOngoing_r, [_0]) } public let Settings_LogoutConfirmationText: String + public let AutoNightTheme_ScheduledTo: String + public let SocksProxySetup_RequiredCredentials: String public let BlockedUsers_Info: String public let ChatSettings_AutomaticAudioDownload: String public let Settings_SetUsername: String @@ -149,6 +151,7 @@ public final class PresentationStrings { public let Message_PinnedInvoice: String public let Login_InfoAvatarAdd: String public let Conversation_RestrictedMedia: String + public let AutoDownloadSettings_LimitBySize: String public let WebSearch_RecentSectionTitle: String private let _CHAT_MESSAGE_TEXT: String private let _CHAT_MESSAGE_TEXT_r: [(Int, NSRange)] @@ -168,6 +171,7 @@ public final class PresentationStrings { public func PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_STICKER, self._PINNED_STICKER_r, [_1, _2]) } + public let AutoDownloadSettings_Title: String public let Conversation_ShareInlineBotLocationConfirmation: String private let _Channel_AdminLog_MessageEdited: String private let _Channel_AdminLog_MessageEdited_r: [(Int, NSRange)] @@ -194,6 +198,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageAdmin, self._Channel_AdminLog_MessageAdmin_r, [_0, _1, _2]) } public let PrivacyLastSeenSettings_NeverShareWith_Placeholder: String + public let Appearance_AutoNightThemeDisabled: String public let TwoStepAuth_SetupEmail: String public let Checkout_PayWithFaceId: String public let Login_ResetAccountProtected_Reset: String @@ -223,6 +228,7 @@ public final class PresentationStrings { public let Paint_Delete: String public let Channel_MessagePhotoUpdated: String public let Cache_Help: String + public let SocksProxySetup_ProxyStatusConnected: String private let _Login_EmailPhoneBody: String private let _Login_EmailPhoneBody_r: [(Int, NSRange)] public func Login_EmailPhoneBody(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -231,6 +237,7 @@ public final class PresentationStrings { public let Checkout_ShippingAddress: String public let Channel_BanList_RestrictedTitle: String public let Checkout_TotalAmount: String + public let Appearance_TextSize: String public let Conversation_MessageEditedLabel: String public let SharedMedia_EmptyLinksText: String private let _Conversation_RestrictedTextTimed: String @@ -261,6 +268,7 @@ public final class PresentationStrings { public let MusicPlayer_VoiceNote: String public let Paint_Duplicate: String public let Channel_Username_InvalidTaken: String + public let Conversation_ClearGroupHistory: String public let Stickers_GroupStickersHelp: String public let SecretChat_Title: String public let Group_UpgradeConfirmation: String @@ -271,6 +279,7 @@ public final class PresentationStrings { 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]) } + public let TermsOfService_DeclineAuthorized: String private let _MESSAGE_GEOLIVE: String private let _MESSAGE_GEOLIVE_r: [(Int, NSRange)] public func MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -303,7 +312,7 @@ public final class PresentationStrings { } public let Month_ShortDecember: String public let Channel_SignMessages: String - public let ChatSettings_AutomaticDownloadVoiceMessage: String + public let Appearance_Title: String public let Conversation_Moderate_Delete: String public let Conversation_CloudStorage_ChatStatus: String public let Login_InfoTitle: String @@ -341,11 +350,14 @@ public final class PresentationStrings { } public let AccessDenied_PhotosRestricted: String public let Map_Locating: String + public let AutoDownloadSettings_Unlimited: String + public let MediaPicker_LivePhotoDescription: 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 TermsOfService_Decline: String public let CallSettings_Always: String public let Message_ImageExpired: String public let Channel_BanUser_Unban: String @@ -376,6 +388,7 @@ public final class PresentationStrings { public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Conversation_EncryptionWaiting, self._Conversation_EncryptionWaiting_r, [_0]) } + public let InfoPlist_NSSiriUsageDescription: String public let Calls_NotNow: String public let Conversation_Report: String private let _CHANNEL_MESSAGE_DOC: String @@ -384,6 +397,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHANNEL_MESSAGE_DOC, self._CHANNEL_MESSAGE_DOC_r, [_1]) } public let Channel_AdminLogFilter_EventsAll: String + public let InfoPlist_NSLocationWhenInUseUsageDescription: String public let Call_ConnectionErrorTitle: String public let Settings_ApplyProxyAlertEnable: String public let Settings_ChatSettings: String @@ -413,6 +427,11 @@ public final class PresentationStrings { public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_PinnedStickerMessage, self._Notification_PinnedStickerMessage_r, [_0]) } + private let _AutoNightTheme_AutomaticHelp: String + private let _AutoNightTheme_AutomaticHelp_r: [(Int, NSRange)] + public func AutoNightTheme_AutomaticHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AutoNightTheme_AutomaticHelp, self._AutoNightTheme_AutomaticHelp_r, [_0]) + } public let PhotoEditor_QualityTool: String public let Login_NetworkError: String public let TwoStepAuth_EnterPasswordForgot: String @@ -457,6 +476,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_MESSAGE_GEO, self._MESSAGE_GEO_r, [_1]) } public let Privacy_Calls: String + public let DialogList_AdLabel: String public let Channel_AdminLogFilter_EventsInfo: String private let _Channel_AdminLog_MessagePinned: String private let _Channel_AdminLog_MessagePinned_r: [(Int, NSRange)] @@ -478,11 +498,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Checkout_SavePasswordTimeoutAndTouchId, self._Checkout_SavePasswordTimeoutAndTouchId_r, [_0]) } public let HashtagSearch_AllChats: String + public let InfoPlist_NSPhotoLibraryAddUsageDescription: String private let _Date_ChatDateHeaderYear: String private let _Date_ChatDateHeaderYear_r: [(Int, NSRange)] public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Date_ChatDateHeaderYear, self._Date_ChatDateHeaderYear_r, [_1, _2, _3]) } + public let Privacy_Calls_P2PContacts: String public let CheckoutInfo_ShippingInfoCountry: String public let Map_ShowPlaces: String public let Camera_VideoMode: String @@ -505,6 +527,7 @@ public final class PresentationStrings { public let Privacy_PaymentsClearInfo: String public let PhotoEditor_CurvesRed: String public let Privacy_PaymentsTitle: String + public let SocksProxySetup_ProxyType: 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)]) { @@ -555,6 +578,7 @@ public final class PresentationStrings { public let AccessDenied_Camera: String public let WatchRemote_NotificationText: String public let SharedMedia_ViewInChat: String + public let SecureId_FormRequestedTitle: String public let Activity_RecordingAudio: String public let Watch_Stickers_StickerPacks: String private let _Target_ShareGameConfirmationPrivate: String @@ -574,6 +598,7 @@ public final class PresentationStrings { public let MediaPicker_VideoMuteDescription: String public let UserInfo_ShareMyContactInfo: String public let Channel_Info_Stickers: String + public let Appearance_ColorTheme: String private let _FileSize_GB: String private let _FileSize_GB_r: [(Int, NSRange)] public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -603,6 +628,7 @@ public final class PresentationStrings { public let ChangePhoneNumberCode_Help: String public let Web_Error: String public let ShareFileTip_Title: String + public let Privacy_SecretChatsLinkPreviews: String public let Username_InvalidStartsWithNumber: String private let _DialogList_EncryptedChatStartedIncoming: String private let _DialogList_EncryptedChatStartedIncoming_r: [(Int, NSRange)] @@ -610,6 +636,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_DialogList_EncryptedChatStartedIncoming, self._DialogList_EncryptedChatStartedIncoming_r, [_0]) } public let Calls_AddTab: String + public let DialogList_AdNoticeAlert: String public let PhotoEditor_TiltShift: String public let ChannelMembers_WhoCanAddMembers_Admins: String public let Tour_Text5: String @@ -630,6 +657,7 @@ public final class PresentationStrings { public let FastTwoStepSetup_EmailHelp: String public let Month_GenOctober: String public let CheckoutInfo_ErrorPhoneInvalid: String + public let AutoNightTheme_UpdateLocation: String public let Group_Setup_TypePublic: String public let Checkout_PaymentMethod_New: String public let ShareMenu_Comment: String @@ -637,6 +665,7 @@ public final class PresentationStrings { public let TwoStepAuth_SetPasswordHelp: String public let Channel_AdminLogFilter_EventsTitle: String public let NotificationSettings_ContactJoined: String + public let ChatSettings_AutoDownloadVideos: String public let Username_LinkCopied: String private let _Time_MonthOfYear_m9: String private let _Time_MonthOfYear_m9_r: [(Int, NSRange)] @@ -654,6 +683,7 @@ public final class PresentationStrings { public let Map_OpenInYandexMaps: String public let FastTwoStepSetup_PasswordHelp: String public let GroupInfo_GroupHistoryHidden: String + public let AutoNightTheme_UseSunsetSunrise: String public let Month_ShortNovember: String public let AccessDenied_Settings: String public let EncryptionKey_Title: String @@ -672,6 +702,7 @@ public final class PresentationStrings { public let Login_InfoFirstNamePlaceholder: String public let Checkout_ErrorProviderAccountInvalid: String public let CallSettings_TabIconDescription: String + public let ChatSettings_AutoDownloadReset: String public let Checkout_WebConfirmation_Title: String public let PasscodeSettings_AutoLock: String public let Notifications_MessageNotificationsPreview: String @@ -694,9 +725,11 @@ public final class PresentationStrings { public let DialogList_DeleteBotConfirmation: String public let EditProfile_Title: String public let PasscodeSettings_HelpTop: String + public let SocksProxySetup_ProxySocks5: String public let Common_TakePhotoOrVideo: String public let Notification_MessageLifetime2s: String public let Checkout_ErrorGeneric: String + public let AutoNightTheme_Automatic: String public let Channel_AdminLog_CanBanUsers: String public let Cache_Indexing: String private let _ENCRYPTION_REQUEST: String @@ -720,8 +753,10 @@ public final class PresentationStrings { public let KeyCommand_FocusOnInputField: String public let Channel_Members_AddAdminErrorBlacklisted: String public let Cache_KeepMedia: String + public let SocksProxySetup_ProxyTelegram: String public let WebPreview_GettingLinkInfo: String public let Group_Setup_TypePublicHelp: String + public let Login_PRIVACY_URL: String public let Map_Satellite: String public let Username_InvalidTaken: String private let _Notification_PinnedAudioMessage: String @@ -779,7 +814,6 @@ public final class PresentationStrings { public let ChannelInfo_DeleteChannelConfirmation: String public let Weekday_ShortSaturday: String public let Map_SendThisLocation: String - public let ChatSettings_AutomaticMediaDownloadMaster: String private let _Notification_PinnedDocumentMessage: String private let _Notification_PinnedDocumentMessage_r: [(Int, NSRange)] public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -817,10 +851,11 @@ public final class PresentationStrings { } public let MaskStickerSettings_Title: String public let TwoStepAuth_SetPassword: String + public let SocksProxySetup_SavedProxies: String public let GroupInfo_InviteLink_ShareLink: String - public let ChatSettings_AutomaticDownloadFile: String public let Common_Cancel: String public let UserInfo_About_Placeholder: String + public let Camera_Discard: String public let ChangePhoneNumberCode_RequestingACall: String public let PrivacyLastSeenSettings_NeverShareWith_Title: String public let KeyCommand_JumpToNextChat: String @@ -830,11 +865,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m8, self._Time_MonthOfYear_m8_r, [_0]) } public let Tour_Text1: String + public let Privacy_SecretChatsTitle: 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 + public let AutoNightTheme_ScheduledFrom: String public let Watch_Conversation_Reply: String public let ShareMenu_CopyShareLink: String public let Stickers_Search: String @@ -861,6 +898,7 @@ public final class PresentationStrings { public func MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_VIDEO, self._MESSAGE_VIDEO_r, [_1]) } + public let TermsOfService_Title: String private let _Checkout_PayPrice: String private let _Checkout_PayPrice_r: [(Int, NSRange)] public func Checkout_PayPrice(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -953,6 +991,7 @@ public final class PresentationStrings { public func Privacy_GroupsAndChannels_InviteToChannelError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Privacy_GroupsAndChannels_InviteToChannelError, self._Privacy_GroupsAndChannels_InviteToChannelError_r, [_0, _1]) } + public let AuthSessions_Sessions: String public let Document_TargetConfirmationFormat: String public let Group_Setup_TypeHeader: String private let _DialogList_SinglePlayingGameSuffix: String @@ -974,11 +1013,15 @@ public final class PresentationStrings { public let ReportPeer_ReasonPornography: String public let Notification_CreatedChannel: String public let PhotoEditor_Original: String + public let TermsOfService_DeclineAndDelete: String public let Target_SelectGroup: String + public let Stickers_SuggestAdded: String public let Channel_AdminLog_InfoPanelAlertTitle: String public let Notifications_GroupNotificationsPreview: String + public let ChatSettings_AutoDownloadPhotos: String public let SecureId_FormFieldEmail: String public let Message_PinnedLocationMessage: String + public let Appearance_PreviewReplyText: String public let Settings_Logout: String private let _UserInfo_BlockConfirmation: String private let _UserInfo_BlockConfirmation_r: [(Int, NSRange)] @@ -987,6 +1030,7 @@ public final class PresentationStrings { } public let Profile_Username: String public let Group_Username_InvalidTooShort: String + public let Appearance_AutoNightTheme: String public let AuthSessions_TerminateOtherSessions: String public let PasscodeSettings_TryAgainIn1Minute: String public let Notifications_InAppNotifications: String @@ -994,8 +1038,10 @@ public final class PresentationStrings { public let EnterPasscode_ChangeTitle: String public let Call_Decline: String public let UserInfo_AddPhone: String + public let AutoNightTheme_Title: String public let Activity_PlayingGame: String public let CheckoutInfo_ShippingInfoStatePlaceholder: String + public let SaveIncomingPhotosSettings_From: String public let Notifications_MessageNotificationsSound: String public let Call_StatusWaiting: String public let SecureId_FormFieldIdentityPlaceholder: String @@ -1011,6 +1057,7 @@ public final class PresentationStrings { } public let ConversationProfile_LeaveDeleteAndExit: String public let State_connecting: String + public let AutoDownloadSettings_PhotosTitle: String public let Map_OpenInHereMaps: String public let Stickers_FavoriteStickers: String public let CheckoutInfo_Pay: String @@ -1024,6 +1071,7 @@ public final class PresentationStrings { } public let Login_SmsRequestState2: String public let Preview_SaveToCameraRoll: String + public let SocksProxySetup_ProxyStatusConnecting: String public let PasscodeSettings_ChangePasscode: String public let TwoStepAuth_RecoveryCodeInvalid: String private let _Message_PaymentSent: String @@ -1040,7 +1088,10 @@ public final class PresentationStrings { } public let Login_InfoDeletePhoto: String public let TwoStepAuth_RecoveryCodeExpired: String + public let AutoDownloadSettings_Channels: String + public let AutoDownloadSettings_Contacts: String public let TwoStepAuth_EmailTitle: String + public let Channel_AdminLog_ChannelEmptyText: String public let Privacy_GroupsAndChannels_NeverAllow: String public let Conversation_RestrictedStickers: String public let Conversation_AddContact: String @@ -1077,6 +1128,7 @@ public final class PresentationStrings { public let PhoneNumberHelp_Help: String public let Channel_LinkItem: String public let Camera_Retake: String + public let StickerPack_ShowStickers: String public let Conversation_RestrictedText: String public let Channel_Stickers_YourStickers: String private let _CHAT_CREATED: String @@ -1092,6 +1144,7 @@ public final class PresentationStrings { } public let ChangePhoneNumberNumber_NewNumber: String public let Compose_NewChannel: String + public let Login_TermsOfServiceAgree: String public let Channel_AdminLog_CanChangeInviteLink: String private let _Call_CallInProgressMessage: String private let _Call_CallInProgressMessage_r: [(Int, NSRange)] @@ -1110,6 +1163,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CancelResetAccount_TextSMS, self._CancelResetAccount_TextSMS_r, [_0]) } public let Channel_EditAdmin_PermissionInviteUsers: String + public let Privacy_Calls_P2PNever: String public let GroupInfo_DeleteAndExit: String public let GroupInfo_InviteLink_CopyLink: String public let Login_ResetAccountProtected_Title: String @@ -1131,6 +1185,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Username_UsernameIsAvailable, self._Username_UsernameIsAvailable_r, [_0]) } public let KeyCommand_JumpToNextUnreadChat: String + public let InfoPlist_NSContactsUsageDescription: String + private let _SocksProxySetup_ProxyStatusPing: String + private let _SocksProxySetup_ProxyStatusPing_r: [(Int, NSRange)] + public func SocksProxySetup_ProxyStatusPing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SocksProxySetup_ProxyStatusPing, self._SocksProxySetup_ProxyStatusPing_r, [_0]) + } private let _Date_ChatDateHeader: String private let _Date_ChatDateHeader_r: [(Int, NSRange)] public func Date_ChatDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1173,11 +1233,13 @@ public final class PresentationStrings { public let Coub_TapForSound: String public let Compose_NewEncryptedChat: String public let PhotoEditor_CropReset: String + public let Privacy_Calls_P2PAlways: String public let Login_InvalidLastNameError: String public let Channel_Members_AddMembers: String public let Tour_Title2: String public let Login_TermsOfServiceHeader: String public let Channel_AdminLog_BanSendGifs: String + public let InfoPlist_NSMicrophoneUsageDescription: String public let AuthSessions_OtherSessions: String public let Watch_UserInfo_Title: String public let InstantPage_FeedbackButton: String @@ -1201,6 +1263,7 @@ public final class PresentationStrings { public let Tour_Text6: String public let PhotoEditor_WarmthTool: String public let Common_TakePhoto: String + public let SocksProxySetup_AdNoticeHelp: String public let UserInfo_CreateNewContact: String public let NetworkUsageSettings_MediaDocumentDataSection: String public let Login_CodeSentCall: String @@ -1229,6 +1292,7 @@ public final class PresentationStrings { public let PhoneLabel_Title: String public let PrivacySettings_Passcode: String public let Paint_ClearConfirm: String + public let SocksProxySetup_Secret: String private let _Checkout_SavePasswordTimeout: String private let _Checkout_SavePasswordTimeout_r: [(Int, NSRange)] public func Checkout_SavePasswordTimeout(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1256,7 +1320,7 @@ public final class PresentationStrings { public let Embed_PlayingInPIP: String public let Localization_EnglishLanguageName: String public let Call_StatusIncoming: String - public let SecureId_FormFieldsHeader: String + public let Settings_Appearance: String public let Settings_PrivacySettings: String public let Conversation_SilentBroadcastTooltipOn: String private let _SecretVideo_NotViewedYet: String @@ -1270,6 +1334,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_MESSAGE_GEO, self._CHAT_MESSAGE_GEO_r, [_1, _2]) } public let DialogList_SearchLabel: String + public let InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String public let Login_CodeSentInternal: String public let Channel_AdminLog_BanSendMessages: String public let Channel_MessagePhotoRemoved: String @@ -1287,12 +1352,14 @@ public final class PresentationStrings { public func LOCKED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_LOCKED_MESSAGE, self._LOCKED_MESSAGE_r, [_1]) } + public let Conversation_ClearPrivateHistory: String 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]) } + public let Conversation_ContextMenuReport: String private let _Call_GroupFormat: String private let _Call_GroupFormat_r: [(Int, NSRange)] public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1300,6 +1367,7 @@ public final class PresentationStrings { } public let Forward_ChannelReadOnly: String public let Privacy_GroupsAndChannels_NeverAllow_Title: String + public let AutoDownloadSettings_Reset: String private let _Channel_AdminLog_MessageInvitedName: String private let _Channel_AdminLog_MessageInvitedName_r: [(Int, NSRange)] public func Channel_AdminLog_MessageInvitedName(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -1319,8 +1387,10 @@ public final class PresentationStrings { return formatWithArgumentRanges(_AuthSessions_AppUnofficial, self._AuthSessions_AppUnofficial_r, [_0]) } public let SecureId_FormFieldEmailPlaceholder: String + public let AutoNightTheme_Disabled: String public let Conversation_ContextMenuBan: String public let Channel_EditAdmin_PermissionsHeader: String + public let SocksProxySetup_PortPlaceholder: String private let _DialogList_SingleUploadingVideoSuffix: String private let _DialogList_SingleUploadingVideoSuffix_r: [(Int, NSRange)] public func DialogList_SingleUploadingVideoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1353,6 +1423,7 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageRestricted, self._Channel_AdminLog_MessageRestricted_r, [_0, _1, _2]) } + public let SocksProxySetup_SecretPlaceholder: String public let Channel_EditAdmin_PermissinAddAdminOn: String public let WebSearch_GIFs: String public let Conversation_SavedMessages: String @@ -1369,6 +1440,8 @@ public final class PresentationStrings { public let Common_Edit: String public let Conversation_OpenFile: String public let Message_PinnedDocumentMessage: String + public let AuthSessions_LogOut: String + public let AutoDownloadSettings_PrivateChats: String public let Checkout_TotalPaidAmount: String public let Conversation_UnsupportedMedia: String private let _Message_ForwardedMessage: String @@ -1390,6 +1463,7 @@ public final class PresentationStrings { public let Profile_CreateEncryptedChatError: String public let Map_LocationTitle: String public let Call_RateCall: String + public let SocksProxySetup_PasswordPlaceholder: String public let Message_ReplyActionButtonShowReceipt: String public let PhotoEditor_ShadowsTool: String public let Checkout_NewCard_CardholderNamePlaceholder: String @@ -1419,7 +1493,6 @@ public final class PresentationStrings { public let Profile_ShareContactButton: String public let Group_ErrorSendRestrictedStickers: String public let Bot_GroupStatusDoesNotReadHistory: String - public let ChatSettings_AutomaticDownloadVideo: String public let Notification_Mute1h: String public let Settings_TabTitle: String public let NetworkUsageSettings_MediaAudioDataSection: String @@ -1490,10 +1563,13 @@ public final class PresentationStrings { public let Call_StatusNoAnswer: String private let _SecureId_FormPolicy: String private let _SecureId_FormPolicy_r: [(Int, NSRange)] - public func SecureId_FormPolicy(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_SecureId_FormPolicy, self._SecureId_FormPolicy_r, [_1, _2]) + public func SecureId_FormPolicy(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SecureId_FormPolicy, self._SecureId_FormPolicy_r, [_0, _1]) } + public let Channel_AdminLogFilter_EventsLeavingSubscribers: String + public let TermsOfService_AgeVerificationTitle: String public let Conversation_MessageDialogDelete: String + public let Appearance_PreviewOutgoingText: String public let Username_Placeholder: String private let _Notification_PinnedDeletedMessage: String private let _Notification_PinnedDeletedMessage_r: [(Int, NSRange)] @@ -1513,6 +1589,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHANNEL_MESSAGE_VIDEO, self._CHANNEL_MESSAGE_VIDEO_r, [_1]) } public let EnterPasscode_TouchId: String + public let AuthSessions_LoggedInWithTelegram: String public let Checkout_ErrorInvoiceAlreadyPaid: String public let ChatAdmins_Title: String public let ChannelMembers_WhoCanAddMembers: String @@ -1539,6 +1616,7 @@ public final class PresentationStrings { public let GroupInfo_InviteLink_RevokeLink: String public let Checkout_PaymentMethod_Title: String public let Conversation_Unmute: String + public let AutoDownloadSettings_DocumentsTitle: String public let Notifications_MessageNotifications: String public let ChannelMembers_WhoCanAddMembersAdminsHelp: String public let DialogList_DeleteBotConversationConfirmation: String @@ -1617,6 +1695,8 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHANNEL_MESSAGE_GIF, self._CHANNEL_MESSAGE_GIF_r, [_1]) } public let Channel_AdminLogFilter_EventsEditedMessages: String + public let AutoNightTheme_ScheduleSection: String + public let Appearance_ThemeNightBlue: String public let Channel_Username_InvalidTooShort: String public let Conversation_ViewGroup: String public let Watch_LastSeen_WithinAWeek: String @@ -1639,6 +1719,7 @@ public final class PresentationStrings { public let Message_LiveLocation: String public let NetworkUsageSettings_Title: String public let CheckoutInfo_ShippingInfoPostcodePlaceholder: String + public let InfoPlist_NSPhotoLibraryUsageDescription: String public let Wallpaper_Wallpaper: String public let GroupInfo_InviteLink_RevokeAlert_Revoke: String public let SharedMedia_TitleLink: String @@ -1666,6 +1747,7 @@ public final class PresentationStrings { public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Watch_Time_ShortTodayAt, self._Watch_Time_ShortTodayAt_r, [_0]) } + public let Channel_AdminLogFilter_EventsNewSubscribers: String public let UserInfo_GroupsInCommon: String public let Message_PinnedContactMessage: String public let AccessDenied_CameraDisabled: String @@ -1713,9 +1795,13 @@ public final class PresentationStrings { public func UserInfo_UnblockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_UserInfo_UnblockConfirmation, self._UserInfo_UnblockConfirmation_r, [_0]) } + public let Appearance_PickAccentColor: String public let UserInfo_ShareBot: String + public let Settings_ProxyConnected: String + public let ChatSettings_AutoDownloadVoiceMessages: String public let TwoStepAuth_EmailSkip: String public let Conversation_JumpToDate: String + public let AutoDownloadSettings_VideoMessagesTitle: String public let CheckoutInfo_ReceiverInfoEmailPlaceholder: String public let Message_Photo: String public let Conversation_ReportSpam: String @@ -1726,6 +1812,7 @@ public final class PresentationStrings { public let DialogList_SearchSectionGlobal: String public let ChangePhoneNumberNumber_NumberPlaceholder: String public let GroupInfo_AddUserLeftError: String + public let Appearance_ThemeDay: String public let GroupInfo_GroupType: String public let Watch_Suggestion_OnMyWay: String public let Checkout_NewCard_PaymentCard: String @@ -1770,6 +1857,7 @@ public final class PresentationStrings { public let GroupInfo_GroupHistory: String public let Conversation_ApplyLocalization: String public let FastTwoStepSetup_Title: String + public let SocksProxySetup_ProxyStatusUnavailable: String public let Conversation_DeleteManyMessages: String public let CancelResetAccount_Title: String public let Notification_CallOutgoingShort: String @@ -1799,6 +1887,7 @@ public final class PresentationStrings { public let Preview_DeletePhoto: String public let GroupInfo_ChannelListNamePlaceholder: String public let PasscodeSettings_TurnPasscodeOn: String + public let AuthSessions_LogOutApplicationsHelp: String private let _Channel_AdminLog_MessageChangedGroupStickerPack: String private let _Channel_AdminLog_MessageChangedGroupStickerPack_r: [(Int, NSRange)] public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1825,7 +1914,9 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageRemovedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageRemovedGroupStickerPack, self._Channel_AdminLog_MessageRemovedGroupStickerPack_r, [_0]) } + public let TermsOfService_Agree: String public let AccessDenied_VideoMessageCamera: String + public let Privacy_ContactsSyncHelp: String public let Conversation_Search: String private let _Channel_Management_PromotedBy: String private let _Channel_Management_PromotedBy_r: [(Int, NSRange)] @@ -1855,6 +1946,7 @@ public final class PresentationStrings { public func CHAT_MESSAGE_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_MESSAGE_CONTACT, self._CHAT_MESSAGE_CONTACT_r, [_1, _2]) } + public let SocksProxySetup_UseProxy: String public let Group_UpgradeNoticeText1: String public let ChatSettings_Other: String private let _Channel_AdminLog_MessageChangedChannelAbout: String @@ -1877,9 +1969,12 @@ public final class PresentationStrings { public let GroupInfo_InviteLink_Help: String public let Calls_Missed: String public let Conversation_ContextMenuForward: String + public let AutoDownloadSettings_ResetHelp: String public let Call_StatusRinging: String public let Invitation_JoinGroup: String public let Notification_PinnedMessage: String + public let AutoDownloadSettings_WiFi: String + public let Conversation_ClearSelfHistory: String public let Message_Location: String private let _Notification_MessageLifetimeChanged: String private let _Notification_MessageLifetimeChanged_r: [(Int, NSRange)] @@ -1917,6 +2012,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_TwoStepAuth_EnterPasswordHint, self._TwoStepAuth_EnterPasswordHint_r, [_0]) } public let CallSettings_TabIcon: String + public let TermsOfService_DeclineUnauthorized: String public let ConversationProfile_UnknownAddMemberError: String private let _Conversation_FileHowToText: String private let _Conversation_FileHowToText_r: [(Int, NSRange)] @@ -1925,6 +2021,7 @@ public final class PresentationStrings { } public let Channel_AdminLog_BanSendMedia: String public let Watch_UserInfo_Unblock: String + public let ChatSettings_AutoDownloadVideoMessages: String public let StickerPacksSettings_ArchivedMasks: String public let Message_Animation: String public let Checkout_PaymentMethod: String @@ -1936,6 +2033,8 @@ public final class PresentationStrings { public func Login_CallRequestState1(_ _0: Int, _ _1: Int) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Login_CallRequestState1, self._Login_CallRequestState1_r, ["\(_0)", String(format: "%.2d", _1)]) } + public let Settings_ProxyDisabled: String + public let SocksProxySetup_Connecting: String public let Channel_Username_CreatePrivateLinkHelp: String private let _Time_PreciseDate_m2: String private let _Time_PreciseDate_m2_r: [(Int, NSRange)] @@ -1955,12 +2054,14 @@ public final class PresentationStrings { public let PhotoEditor_SaturationTool: String public let Channel_BanUser_BlockFor: String public let Call_StatusConnecting: String + public let AutoNightTheme_NotAvailable: String public let Bot_Start: String private let _Channel_AdminLog_MessageChangedGroupAbout: String private let _Channel_AdminLog_MessageChangedGroupAbout_r: [(Int, NSRange)] public func Channel_AdminLog_MessageChangedGroupAbout(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedGroupAbout, self._Channel_AdminLog_MessageChangedGroupAbout_r, [_0]) } + public let Appearance_PreviewReplyAuthor: String public let Notifications_TextTone: String public let Settings_CallSettings: String private let _Watch_Time_ShortYesterdayAt: String @@ -1979,6 +2080,7 @@ public final class PresentationStrings { public let Channel_EditAdmin_PermissionDeleteMessages: String public let Channel_BanUser_PermissionSendStickersAndGifs: String public let Conversation_CloudStorageInfo_Title: String + public let Conversation_ClearSecretHistory: String public let Notification_RenamedChannel: String public let BlockedUsers_BlockUser: String public let ChatSettings_TextSize: String @@ -1996,6 +2098,7 @@ public final class PresentationStrings { public let PhotoEditor_TintTool: String public let Watch_Suggestion_CantTalk: String public let PhotoEditor_QualityHigh: String + public let SocksProxySetup_AddProxyTitle: String private let _CHAT_MESSAGE_STICKER: String private let _CHAT_MESSAGE_STICKER_r: [(Int, NSRange)] public func CHAT_MESSAGE_STICKER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { @@ -2007,7 +2110,9 @@ public final class PresentationStrings { public let Channel_About_Help: String public let Web_OpenExternal: String public let UserInfo_AddContact: String + public let Privacy_ContactsSync: String public let SocksProxySetup_Connection: String + public let SocksProxySetup_ProxyStatusChecking: String public let Call_EncryptionKey_Title: String public let PhotoEditor_BlurToolLinear: String public let AuthSessions_EmptyText: String @@ -2052,6 +2157,7 @@ public final class PresentationStrings { public let Map_YouAreHere: String public let PhotoEditor_CurvesTool: String public let Map_LiveLocationFor1Hour: String + public let AutoNightTheme_AutomaticSection: String public let Stickers_NoStickersFound: String private let _Notification_JoinedChannel: String private let _Notification_JoinedChannel_r: [(Int, NSRange)] @@ -2060,6 +2166,7 @@ public final class PresentationStrings { } public let GroupInfo_ActionRestrict: String public let Checkout_ShippingOption_Title: String + public let Stickers_SuggestStickers: 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)]) { @@ -2072,6 +2179,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_ADD_MEMBER, self._CHAT_ADD_MEMBER_r, [_1, _2, _3]) } public let Weekday_ShortSunday: String + public let Privacy_ContactsResetConfirmation: String public let Month_ShortJune: String public let Privacy_Calls_Integration: String public let Channel_TypeSetup_Title: String @@ -2151,6 +2259,7 @@ public final class PresentationStrings { public let Conversation_AddToReadingList: String public let Conversation_FileDropbox: String public let Login_PhonePlaceholder: String + public let SocksProxySetup_ProxyEnabled: String public let Profile_MessageLifetime1d: String public let CheckoutInfo_ShippingInfoCityPlaceholder: String public let Calls_CallTabDescription: String @@ -2166,6 +2275,7 @@ public final class PresentationStrings { public func Time_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_TodayAt, self._Time_TodayAt_r, [_0]) } + public let AutoDownloadSettings_VideosTitle: String public let Conversation_Processing: String public let Conversation_RestrictedInline: String private let _InstantPage_AuthorAndDateTitle: String @@ -2227,7 +2337,9 @@ public final class PresentationStrings { public let Tour_Text3: String public let Contacts_GlobalSearch: String public let DialogList_LanguageTooltip: String + public let AuthSessions_LogOutApplications: String public let Map_LoadError: String + public let Settings_ProxyConnecting: String public let AccessDenied_VoiceMicrophone: String private let _CHANNEL_MESSAGE_STICKER: String private let _CHANNEL_MESSAGE_STICKER_r: [(Int, NSRange)] @@ -2241,10 +2353,11 @@ public final class PresentationStrings { public let Channel_Status: String public let Map_ChooseLocationTitle: String public let Map_OpenInYandexNavigator: String - public let ChatSettings_AutomaticDownloadPhoto: String + public let AutoNightTheme_PreferredTheme: String public let State_WaitingForNetwork: String public let TwoStepAuth_EmailHelp: String public let Conversation_StopLiveLocation: String + public let Privacy_SecretChatsLinkPreviewsHelp: String public let PhotoEditor_SharpenTool: String public let Common_of: String public let AuthSessions_Title: String @@ -2288,6 +2401,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_CHAT_ADD_YOU, self._CHAT_ADD_YOU_r, [_1, _2]) } public let CheckoutInfo_ShippingInfoCity: String + public let AutoDownloadSettings_GroupChats: String public let Conversation_ClousStorageInfo_Description3: String public let Conversation_PinMessageAlertGroup: String public let Settings_FAQ_Intro: String @@ -2314,10 +2428,11 @@ public final class PresentationStrings { private let _Checkout_LiabilityAlert: String private let _Checkout_LiabilityAlert_r: [(Int, NSRange)] public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _2]) + return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1, _1, _2]) } public let Channel_Info_BlackList: String public let Profile_BotInfo: String + public let Stickers_SuggestAll: String public let Compose_NewChannel_Members: String public let Notification_Reply: String public let Watch_Stickers_Recents: String @@ -2330,6 +2445,12 @@ public final class PresentationStrings { return formatWithArgumentRanges(_MESSAGE_STICKER, self._MESSAGE_STICKER_r, [_1, _2]) } public let Profile_MessageLifetime5s: String + public let Privacy_ContactsReset: String + private let _TermsOfService_AgeVerificationText: String + private let _TermsOfService_AgeVerificationText_r: [(Int, NSRange)] + public func TermsOfService_AgeVerificationText(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_TermsOfService_AgeVerificationText, self._TermsOfService_AgeVerificationText_r, ["\(_0)"]) + } private let _PINNED_PHOTO: String private let _PINNED_PHOTO_r: [(Int, NSRange)] public func PINNED_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2338,10 +2459,13 @@ public final class PresentationStrings { public let Channel_AdminLog_CanAddAdmins: String public let TwoStepAuth_SetupHint: String public let Conversation_StatusLeftGroup: String + public let ChatSettings_AutoDownloadDocuments: String public let MediaPicker_TapToUngroupDescription: String public let Conversation_ShareBotLocationConfirmation: String public let Conversation_DeleteMessagesForMe: String public let Message_PinnedAnimationMessage: String + public let SocksProxySetup_ConnectAndSave: String + public let SocksProxySetup_FailedToConnect: String public let Checkout_ErrorPrecheckoutFailed: String public let Camera_PhotoMode: String private let _Time_MonthOfYear_m2: String @@ -2365,6 +2489,7 @@ public final class PresentationStrings { public let Channel_ErrorAccessDenied: String public let Generic_ErrorMoreInfo: String public let Channel_AdminLog_TitleAllEvents: String + public let Settings_Proxy: String public let ChannelMembers_WhoCanAddMembersAllHelp: String public let ChangePhoneNumberCode_CodePlaceholder: String public let Camera_SquareMode: String @@ -2377,6 +2502,7 @@ public final class PresentationStrings { public let Login_PadPhoneHelpTitle: String public let Profile_CreateNewContact: String public let AccessDenied_VideoMessageMicrophone: String + public let AutoDownloadSettings_VoiceMessagesTitle: String public let PhotoEditor_VignetteTool: String public let LastSeen_WithinAWeek: String public let Widget_NoUsers: String @@ -2388,6 +2514,7 @@ public final class PresentationStrings { } public let DialogList_NoMessagesText: String public let MaskStickerSettings_Info: String + public let ChatSettings_AutoDownloadTitle: String public let Conversation_FilePhotoOrVideo: String public let Channel_AdminLog_BanSendStickers: String public let Common_Next: String @@ -2400,6 +2527,7 @@ public final class PresentationStrings { } public let GroupInfo_DeleteAndExitConfirmation: String public let TwoStepAuth_EmailInvalid: String + public let Privacy_ContactsTitle: String private let _CHAT_MESSAGE_VIDEO: String private let _CHAT_MESSAGE_VIDEO_r: [(Int, NSRange)] public func CHAT_MESSAGE_VIDEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -2427,6 +2555,11 @@ public final class PresentationStrings { public let DialogList_RecentTitlePeople: String public let GroupInfo_Notifications: String public let Call_ReportPlaceholder: String + private let _AuthSessions_Message: String + private let _AuthSessions_Message_r: [(Int, NSRange)] + public func AuthSessions_Message(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AuthSessions_Message, self._AuthSessions_Message_r, [_0]) + } private let _MESSAGE_DOC: String private let _MESSAGE_DOC_r: [(Int, NSRange)] public func MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2487,7 +2620,6 @@ public final class PresentationStrings { public let Channel_AdminLog_EmptyFilterText: String public let Channel_AdminLog_EmptyText: String public let PrivacySettings_DeleteAccountTitle: String - public let Peer_DeletedUser: String public let PrivacyLastSeenSettings_CustomShareSettings_Delete: String private let _ENCRYPTED_MESSAGE: String private let _ENCRYPTED_MESSAGE_r: [(Int, NSRange)] @@ -2527,6 +2659,7 @@ public final class PresentationStrings { } public let Group_EditAdmin_PermissionChangeInfo: String public let TwoStepAuth_Email: String + public let Stickers_SuggestNone: String public let Map_SendMyCurrentLocation: String private let _MESSAGE_ROUND: String private let _MESSAGE_ROUND_r: [(Int, NSRange)] @@ -2538,6 +2671,7 @@ public final class PresentationStrings { public let AccessDenied_Title: String public let SharedMedia_CategoryLinks: String public let Localization_LanguageOther: String + public let SaveIncomingPhotosSettings_Title: String public let TwoStepAuth_EmailSkipAlert: String public let ChatSettings_Stickers: String public let Camera_FlashOff: String @@ -2567,7 +2701,6 @@ public final class PresentationStrings { public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Login_EmailCodeBody, self._Login_EmailCodeBody_r, [_0]) } - public let ChatSettings_AutomaticDownloadVideoMessage: String public let Profile_About: String private let _EncryptionKey_Description: String private let _EncryptionKey_Description_r: [(Int, NSRange)] @@ -2599,9 +2732,11 @@ public final class PresentationStrings { public func Watch_Time_ShortWeekdayAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Watch_Time_ShortWeekdayAt, self._Watch_Time_ShortWeekdayAt_r, [_1, _2]) } + public let Conversation_EmptyGifPanelPlaceholder: String public let DialogList_Typing: String public let Notification_CallBack: String public let Map_LocatingError: String + public let InfoPlist_NSFaceIDUsageDescription: String public let MediaPicker_Send: String public let ChannelIntro_Title: String public let AccessDenied_LocationAlwaysDenied: String @@ -2618,14 +2753,18 @@ public final class PresentationStrings { public let Channel_EditAdmin_CannotEdit: String public let LoginPassword_PasswordHelp: String public let BlockedUsers_Unblock: String + public let AutoDownloadSettings_Cellular: 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 Appearance_PreviewIncomingText: String public let Notifications_GroupNotificationsAlert: String public let Paint_Masks: String + public let Appearance_ThemeDayClassic: String public let StickerPack_ErrorNotFound: String + public let Appearance_ThemeNight: String public let SecretTimer_ImageDescription: String private let _PINNED_CONTACT: String private let _PINNED_CONTACT_r: [(Int, NSRange)] @@ -2642,6 +2781,7 @@ public final class PresentationStrings { public let Channel_AdminLog_EmptyTitle: String public let PhotoEditor_Set: String public let LiveLocation_MenuStopAll: String + public let SocksProxySetup_AddProxy: String private let _Notification_Invited: String private let _Notification_Invited_r: [(Int, NSRange)] public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { @@ -2652,6 +2792,7 @@ public final class PresentationStrings { public let AppleWatch_ReplyPresets: String public let Channel_Members_AddAdminErrorNotAMember: String public let Conversation_EncryptedDescription2: String + public let SocksProxySetup_HostnamePlaceholder: String public let NetworkUsageSettings_MediaVideoDataSection: String public let Paint_Edit: String public let Conversation_EncryptedDescription3: String @@ -2666,8 +2807,8 @@ public final class PresentationStrings { public let AuthSessions_CurrentSession: String private let _SecureId_RequestTitle: String private let _SecureId_RequestTitle_r: [(Int, NSRange)] - public func SecureId_RequestTitle(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_SecureId_RequestTitle, self._SecureId_RequestTitle_r, [_1]) + public func SecureId_RequestTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SecureId_RequestTitle, self._SecureId_RequestTitle_r, [_0]) } public let Watch_Microphone_Access: String private let _Notification_RenamedChat: String @@ -2683,6 +2824,11 @@ public final class PresentationStrings { public let ShareMenu_ShareTo: String public let Message_PinnedGame: String public let Channel_AdminLog_CanSendMessages: String + private let _AutoNightTheme_LocationHelp: String + private let _AutoNightTheme_LocationHelp_r: [(Int, NSRange)] + public func AutoNightTheme_LocationHelp(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AutoNightTheme_LocationHelp, self._AutoNightTheme_LocationHelp_r, [_0, _1]) + } public let Notification_RenamedGroup: String private let _Call_PrivacyErrorMessage: String private let _Call_PrivacyErrorMessage_r: [(Int, NSRange)] @@ -2697,6 +2843,7 @@ public final class PresentationStrings { public let Preview_DeleteGif: String public let UserInfo_DeleteContact: String public let Notifications_ResetAllNotifications: String + public let SocksProxySetup_SaveProxy: String public let Notification_MessageLifetimeRemovedOutgoing: String public let Login_ContinueWithLocalization: String public let GroupInfo_AddParticipant: String @@ -2795,6 +2942,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_ChannelInfo_ChannelForbidden, self._ChannelInfo_ChannelForbidden_r, [_0]) } public let Conversation_ShareMyContactInfo: String + public let SocksProxySetup_UsernamePlaceholder: String private let _CHANNEL_MESSAGE_GEO: String private let _CHANNEL_MESSAGE_GEO_r: [(Int, NSRange)] public func CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2803,7 +2951,9 @@ public final class PresentationStrings { public let Contacts_PhoneNumber: String public let Group_Info_AdminLog: String public let Channel_AdminLogFilter_ChannelEventsInfo: String + public let ChatSettings_AutoDownloadEnabled: String public let StickerPacksSettings_FeaturedPacks: String + public let AuthSessions_LoggedIn: String public let Month_GenAugust: String public let Notification_CallCanceled: String public let Channel_Username_CreatePublicLinkHelp: String @@ -2820,6 +2970,7 @@ public final class PresentationStrings { public let FastTwoStepSetup_PasswordConfirmationPlaceholder: String public let PasscodeSettings_Title: String public let StickerPack_BuiltinPackName: String + public let Appearance_AccentColor: String public let Watch_Suggestion_BRB: String private let _CHAT_MESSAGE_ROUND: String private let _CHAT_MESSAGE_ROUND_r: [(Int, NSRange)] @@ -2831,6 +2982,7 @@ public final class PresentationStrings { public let GroupInfo_LabelAdmin: String public let GroupInfo_Sound: String public let Channel_EditAdmin_PermissionBanUsers: String + public let InfoPlist_NSCameraUsageDescription: String public let Wallpaper_PhotoLibrary: String public let Settings_About: String public let Privacy_Calls_IntegrationHelp: String @@ -2845,6 +2997,7 @@ public final class PresentationStrings { public func Map_LiveLocationShortHour(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Map_LiveLocationShortHour, self._Map_LiveLocationShortHour_r, [_0]) } + public let Appearance_Preview: String private let _DialogList_AwaitingEncryption: String private let _DialogList_AwaitingEncryption_r: [(Int, NSRange)] public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2888,6 +3041,11 @@ public final class PresentationStrings { public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_Username_LinkHint, self._Channel_Username_LinkHint_r, [_0]) } + private let _AutoDownloadSettings_UpTo: String + private let _AutoDownloadSettings_UpTo_r: [(Int, NSRange)] + public func AutoDownloadSettings_UpTo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AutoDownloadSettings_UpTo, self._AutoDownloadSettings_UpTo_r, [_0]) + } public let Settings_ViewPhoto: String public let Paint_RecentStickers: String public let Login_CallRequestState3: String @@ -2909,12 +3067,12 @@ public final class PresentationStrings { public let Tour_Text4: String public let Channel_Info_Description: String public let AccessDenied_LocationTracking: String + public let TermsOfService_Disagree: String public let Watch_Compose_Send: String public let SocksProxySetup_UseForCallsHelp: String public let Preview_CopyAddress: String public let Settings_BlockedUsers: String public let Month_ShortAugust: String - public let ChatSettings_AutomaticMediaDownload: String public let Channel_AdminLogFilter_AdminsTitle: String public let Channel_EditAdmin_PermissionChangeInfo: String public let Notifications_ResetAllNotificationsHelp: String @@ -2988,7 +3146,6 @@ public final class PresentationStrings { public let Group_ErrorAddBlocked: String public let TwoStepAuth_AdditionalPassword: String public let MediaPicker_Videos: String - public let ChatSettings_AutomaticDownloadReset: String public let BlockedUsers_AddNew: String public let StickerPacksSettings_StickerPacksSection: String public let Channel_NotificationLoading: String @@ -3004,6 +3161,8 @@ public final class PresentationStrings { public let Checkout_EnterPassword: String public let StickerPack_HideStickers: String public let UserInfo_NotificationsEnabled: String + public let InfoPlist_NSLocationAlwaysUsageDescription: String + public let SocksProxySetup_ProxyDetailsTitle: String public let Weekday_ShortTuesday: String public let Notification_CallIncomingShort: String public let ConvertToSupergroup_Note: String @@ -3012,404 +3171,10 @@ public final class PresentationStrings { public let StickerSettings_ContextHide: String public let Media_ShareThisPhoto: String public let Contacts_ShareTelegram: String + public let AutoNightTheme_Scheduled: String public let PrivacySettings_PasscodeAndFaceId: String public let Settings_ChatBackground: String - private let _MessageTimer_Seconds_zero: String - private let _MessageTimer_Seconds_one: String - private let _MessageTimer_Seconds_two: String - private let _MessageTimer_Seconds_few: String - private let _MessageTimer_Seconds_many: String - private let _MessageTimer_Seconds_other: String - public func MessageTimer_Seconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Seconds_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Seconds_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Seconds_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Seconds_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Seconds_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Seconds_other, "\(value)") - } - } - private let _Call_Seconds_zero: String - private let _Call_Seconds_one: String - private let _Call_Seconds_two: String - private let _Call_Seconds_few: String - private let _Call_Seconds_many: String - private let _Call_Seconds_other: String - public func Call_Seconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_Seconds_zero, "\(value)") - case .one: - return String(format: self._Call_Seconds_one, "\(value)") - case .two: - return String(format: self._Call_Seconds_two, "\(value)") - case .few: - return String(format: self._Call_Seconds_few, "\(value)") - case .many: - return String(format: self._Call_Seconds_many, "\(value)") - case .other: - return String(format: self._Call_Seconds_other, "\(value)") - } - } - private let _MessageTimer_ShortSeconds_zero: String - private let _MessageTimer_ShortSeconds_one: String - private let _MessageTimer_ShortSeconds_two: String - private let _MessageTimer_ShortSeconds_few: String - private let _MessageTimer_ShortSeconds_many: String - private let _MessageTimer_ShortSeconds_other: String - public func MessageTimer_ShortSeconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortSeconds_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") - } - } - private let _Notification_GameScoreExtended_zero: String - private let _Notification_GameScoreExtended_one: String - private let _Notification_GameScoreExtended_two: String - private let _Notification_GameScoreExtended_few: String - private let _Notification_GameScoreExtended_many: String - private let _Notification_GameScoreExtended_other: String - public func Notification_GameScoreExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreExtended_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreExtended_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreExtended_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreExtended_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreExtended_many, "\(value)") - case .other: - 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 - private let _PasscodeSettings_FailedAttempts_few: String - private let _PasscodeSettings_FailedAttempts_many: String - private let _PasscodeSettings_FailedAttempts_other: String - public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._PasscodeSettings_FailedAttempts_zero, "\(value)") - case .one: - return String(format: self._PasscodeSettings_FailedAttempts_one, "\(value)") - case .two: - return String(format: self._PasscodeSettings_FailedAttempts_two, "\(value)") - case .few: - return String(format: self._PasscodeSettings_FailedAttempts_few, "\(value)") - case .many: - return String(format: self._PasscodeSettings_FailedAttempts_many, "\(value)") - case .other: - return String(format: self._PasscodeSettings_FailedAttempts_other, "\(value)") - } - } - private let _MuteFor_Hours_zero: String - private let _MuteFor_Hours_one: String - private let _MuteFor_Hours_two: String - private let _MuteFor_Hours_few: String - private let _MuteFor_Hours_many: String - private let _MuteFor_Hours_other: String - public func MuteFor_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteFor_Hours_zero, "\(value)") - case .one: - return String(format: self._MuteFor_Hours_one, "\(value)") - case .two: - return String(format: self._MuteFor_Hours_two, "\(value)") - case .few: - return String(format: self._MuteFor_Hours_few, "\(value)") - case .many: - return String(format: self._MuteFor_Hours_many, "\(value)") - case .other: - return String(format: self._MuteFor_Hours_other, "\(value)") - } - } - private let _Media_ShareVideo_zero: String - private let _Media_ShareVideo_one: String - private let _Media_ShareVideo_two: String - private let _Media_ShareVideo_few: String - private let _Media_ShareVideo_many: String - private let _Media_ShareVideo_other: String - public func Media_ShareVideo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Media_ShareVideo_zero, "\(value)") - case .one: - return String(format: self._Media_ShareVideo_one, "\(value)") - case .two: - return String(format: self._Media_ShareVideo_two, "\(value)") - case .few: - return String(format: self._Media_ShareVideo_few, "\(value)") - case .many: - return String(format: self._Media_ShareVideo_many, "\(value)") - case .other: - return String(format: self._Media_ShareVideo_other, "\(value)") - } - } - private let _MessageTimer_ShortMinutes_zero: String - private let _MessageTimer_ShortMinutes_one: String - private let _MessageTimer_ShortMinutes_two: String - private let _MessageTimer_ShortMinutes_few: String - private let _MessageTimer_ShortMinutes_many: String - private let _MessageTimer_ShortMinutes_other: String - public func MessageTimer_ShortMinutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortMinutes_other, "\(value)") - } - } - private let _Notification_GameScoreSelfExtended_zero: String - private let _Notification_GameScoreSelfExtended_one: String - private let _Notification_GameScoreSelfExtended_two: String - private let _Notification_GameScoreSelfExtended_few: String - private let _Notification_GameScoreSelfExtended_many: String - private let _Notification_GameScoreSelfExtended_other: String - public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") - } - } - private let _MessageTimer_ShortDays_zero: String - private let _MessageTimer_ShortDays_one: String - private let _MessageTimer_ShortDays_two: String - private let _MessageTimer_ShortDays_few: String - private let _MessageTimer_ShortDays_many: String - private let _MessageTimer_ShortDays_other: String - public func MessageTimer_ShortDays(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortDays_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortDays_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortDays_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortDays_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortDays_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortDays_other, "\(value)") - } - } - private let _GroupInfo_ParticipantCount_zero: String - private let _GroupInfo_ParticipantCount_one: String - private let _GroupInfo_ParticipantCount_two: String - private let _GroupInfo_ParticipantCount_few: String - private let _GroupInfo_ParticipantCount_many: String - private let _GroupInfo_ParticipantCount_other: String - public func GroupInfo_ParticipantCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._GroupInfo_ParticipantCount_zero, "\(value)") - case .one: - return String(format: self._GroupInfo_ParticipantCount_one, "\(value)") - case .two: - return String(format: self._GroupInfo_ParticipantCount_two, "\(value)") - case .few: - return String(format: self._GroupInfo_ParticipantCount_few, "\(value)") - case .many: - return String(format: self._GroupInfo_ParticipantCount_many, "\(value)") - case .other: - return String(format: self._GroupInfo_ParticipantCount_other, "\(value)") - } - } - private let _ForwardedPhotos_zero: String - private let _ForwardedPhotos_one: String - private let _ForwardedPhotos_two: String - private let _ForwardedPhotos_few: String - private let _ForwardedPhotos_many: String - private let _ForwardedPhotos_other: String - public func ForwardedPhotos(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedPhotos_zero, "\(value)") - case .one: - return String(format: self._ForwardedPhotos_one, "\(value)") - case .two: - return String(format: self._ForwardedPhotos_two, "\(value)") - case .few: - return String(format: self._ForwardedPhotos_few, "\(value)") - case .many: - return String(format: self._ForwardedPhotos_many, "\(value)") - case .other: - return String(format: self._ForwardedPhotos_other, "\(value)") - } - } - private let _ServiceMessage_GameScoreSelfExtended_zero: String - private let _ServiceMessage_GameScoreSelfExtended_one: String - private let _ServiceMessage_GameScoreSelfExtended_two: String - private let _ServiceMessage_GameScoreSelfExtended_few: String - private let _ServiceMessage_GameScoreSelfExtended_many: String - private let _ServiceMessage_GameScoreSelfExtended_other: String - public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreSelfExtended_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreSelfExtended_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreSelfExtended_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreSelfExtended_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreSelfExtended_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreSelfExtended_other, "\(value)") - } - } - private let _Call_ShortSeconds_zero: String - private let _Call_ShortSeconds_one: String - private let _Call_ShortSeconds_two: String - private let _Call_ShortSeconds_few: String - private let _Call_ShortSeconds_many: String - private let _Call_ShortSeconds_other: String - public func Call_ShortSeconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_ShortSeconds_zero, "\(value)") - case .one: - return String(format: self._Call_ShortSeconds_one, "\(value)") - case .two: - return String(format: self._Call_ShortSeconds_two, "\(value)") - case .few: - return String(format: self._Call_ShortSeconds_few, "\(value)") - case .many: - return String(format: self._Call_ShortSeconds_many, "\(value)") - case .other: - return String(format: self._Call_ShortSeconds_other, "\(value)") - } - } - private let _Conversation_StatusSubscribers_zero: String - private let _Conversation_StatusSubscribers_one: String - private let _Conversation_StatusSubscribers_two: String - private let _Conversation_StatusSubscribers_few: String - private let _Conversation_StatusSubscribers_many: String - private let _Conversation_StatusSubscribers_other: String - public func Conversation_StatusSubscribers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_StatusSubscribers_zero, "\(value)") - case .one: - return String(format: self._Conversation_StatusSubscribers_one, "\(value)") - case .two: - return String(format: self._Conversation_StatusSubscribers_two, "\(value)") - case .few: - return String(format: self._Conversation_StatusSubscribers_few, "\(value)") - case .many: - return String(format: self._Conversation_StatusSubscribers_many, "\(value)") - case .other: - return String(format: self._Conversation_StatusSubscribers_other, "\(value)") - } - } - private let _SharedMedia_File_zero: String - private let _SharedMedia_File_one: String - private let _SharedMedia_File_two: String - private let _SharedMedia_File_few: String - private let _SharedMedia_File_many: String - private let _SharedMedia_File_other: String - public func SharedMedia_File(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_File_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_File_one, "\(value)") - case .two: - return String(format: self._SharedMedia_File_two, "\(value)") - case .few: - return String(format: self._SharedMedia_File_few, "\(value)") - case .many: - return String(format: self._SharedMedia_File_many, "\(value)") - case .other: - return String(format: self._SharedMedia_File_other, "\(value)") - } - } - private let _ForwardedAudios_zero: String - private let _ForwardedAudios_one: String - private let _ForwardedAudios_two: String - private let _ForwardedAudios_few: String - private let _ForwardedAudios_many: String - private let _ForwardedAudios_other: String - public func ForwardedAudios(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedAudios_zero, "\(value)") - case .one: - return String(format: self._ForwardedAudios_one, "\(value)") - case .two: - return String(format: self._ForwardedAudios_two, "\(value)") - case .few: - return String(format: self._ForwardedAudios_few, "\(value)") - case .many: - return String(format: self._ForwardedAudios_many, "\(value)") - case .other: - return String(format: self._ForwardedAudios_other, "\(value)") - } - } + public let TermsOfService_Confirm: String private let _PrivacyLastSeenSettings_AddUsers_zero: String private let _PrivacyLastSeenSettings_AddUsers_one: String private let _PrivacyLastSeenSettings_AddUsers_two: String @@ -3432,1040 +3197,6 @@ public final class PresentationStrings { return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") } } - private let _ForwardedVideoMessages_zero: String - private let _ForwardedVideoMessages_one: String - private let _ForwardedVideoMessages_two: String - private let _ForwardedVideoMessages_few: String - private let _ForwardedVideoMessages_many: String - private let _ForwardedVideoMessages_other: String - public func ForwardedVideoMessages(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedVideoMessages_zero, "\(value)") - case .one: - return String(format: self._ForwardedVideoMessages_one, "\(value)") - case .two: - return String(format: self._ForwardedVideoMessages_two, "\(value)") - case .few: - return String(format: self._ForwardedVideoMessages_few, "\(value)") - case .many: - return String(format: self._ForwardedVideoMessages_many, "\(value)") - case .other: - return String(format: self._ForwardedVideoMessages_other, "\(value)") - } - } - private let _SharedMedia_Generic_zero: String - private let _SharedMedia_Generic_one: String - private let _SharedMedia_Generic_two: String - private let _SharedMedia_Generic_few: String - private let _SharedMedia_Generic_many: String - private let _SharedMedia_Generic_other: String - public func SharedMedia_Generic(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Generic_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Generic_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Generic_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Generic_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Generic_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Generic_other, "\(value)") - } - } - private let _InviteText_ContactsCount_zero: String - private let _InviteText_ContactsCount_one: String - private let _InviteText_ContactsCount_two: String - private let _InviteText_ContactsCount_few: String - private let _InviteText_ContactsCount_many: String - private let _InviteText_ContactsCount_other: String - public func InviteText_ContactsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._InviteText_ContactsCount_zero, "\(value)") - case .one: - return String(format: self._InviteText_ContactsCount_one, "\(value)") - case .two: - return String(format: self._InviteText_ContactsCount_two, "\(value)") - case .few: - return String(format: self._InviteText_ContactsCount_few, "\(value)") - case .many: - return String(format: self._InviteText_ContactsCount_many, "\(value)") - case .other: - return String(format: self._InviteText_ContactsCount_other, "\(value)") - } - } - private let _Conversation_StatusMembers_zero: String - private let _Conversation_StatusMembers_one: String - private let _Conversation_StatusMembers_two: String - private let _Conversation_StatusMembers_few: String - private let _Conversation_StatusMembers_many: String - private let _Conversation_StatusMembers_other: String - public func Conversation_StatusMembers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_StatusMembers_zero, "\(value)") - case .one: - return String(format: self._Conversation_StatusMembers_one, "\(value)") - case .two: - return String(format: self._Conversation_StatusMembers_two, "\(value)") - case .few: - return String(format: self._Conversation_StatusMembers_few, "\(value)") - case .many: - return String(format: self._Conversation_StatusMembers_many, "\(value)") - case .other: - return String(format: self._Conversation_StatusMembers_other, "\(value)") - } - } - private let _Conversation_LiveLocationMembersCount_zero: String - private let _Conversation_LiveLocationMembersCount_one: String - private let _Conversation_LiveLocationMembersCount_two: String - private let _Conversation_LiveLocationMembersCount_few: String - private let _Conversation_LiveLocationMembersCount_many: String - private let _Conversation_LiveLocationMembersCount_other: String - public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_LiveLocationMembersCount_zero, "\(value)") - case .one: - return String(format: self._Conversation_LiveLocationMembersCount_one, "\(value)") - case .two: - return String(format: self._Conversation_LiveLocationMembersCount_two, "\(value)") - case .few: - return String(format: self._Conversation_LiveLocationMembersCount_few, "\(value)") - case .many: - return String(format: self._Conversation_LiveLocationMembersCount_many, "\(value)") - case .other: - return String(format: self._Conversation_LiveLocationMembersCount_other, "\(value)") - } - } - private let _Media_SharePhoto_zero: String - private let _Media_SharePhoto_one: String - private let _Media_SharePhoto_two: String - private let _Media_SharePhoto_few: String - private let _Media_SharePhoto_many: String - private let _Media_SharePhoto_other: String - public func Media_SharePhoto(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Media_SharePhoto_zero, "\(value)") - case .one: - return String(format: self._Media_SharePhoto_one, "\(value)") - case .two: - return String(format: self._Media_SharePhoto_two, "\(value)") - case .few: - return String(format: self._Media_SharePhoto_few, "\(value)") - case .many: - return String(format: self._Media_SharePhoto_many, "\(value)") - case .other: - return String(format: self._Media_SharePhoto_other, "\(value)") - } - } - private let _LiveLocation_MenuChatsCount_zero: String - private let _LiveLocation_MenuChatsCount_one: String - private let _LiveLocation_MenuChatsCount_two: String - private let _LiveLocation_MenuChatsCount_few: String - private let _LiveLocation_MenuChatsCount_many: String - private let _LiveLocation_MenuChatsCount_other: String - public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LiveLocation_MenuChatsCount_zero, "\(value)") - case .one: - return String(format: self._LiveLocation_MenuChatsCount_one, "\(value)") - case .two: - return String(format: self._LiveLocation_MenuChatsCount_two, "\(value)") - case .few: - return String(format: self._LiveLocation_MenuChatsCount_few, "\(value)") - case .many: - return String(format: self._LiveLocation_MenuChatsCount_many, "\(value)") - case .other: - return String(format: self._LiveLocation_MenuChatsCount_other, "\(value)") - } - } - private let _Invitation_Members_zero: String - private let _Invitation_Members_one: String - private let _Invitation_Members_two: String - private let _Invitation_Members_few: String - private let _Invitation_Members_many: String - private let _Invitation_Members_other: String - public func Invitation_Members(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Invitation_Members_zero, "\(value)") - case .one: - return String(format: self._Invitation_Members_one, "\(value)") - case .two: - return String(format: self._Invitation_Members_two, "\(value)") - case .few: - return String(format: self._Invitation_Members_few, "\(value)") - case .many: - return String(format: self._Invitation_Members_many, "\(value)") - case .other: - return String(format: self._Invitation_Members_other, "\(value)") - } - } - private let _ForwardedFiles_zero: String - private let _ForwardedFiles_one: String - private let _ForwardedFiles_two: String - private let _ForwardedFiles_few: String - private let _ForwardedFiles_many: String - private let _ForwardedFiles_other: String - public func ForwardedFiles(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedFiles_zero, "\(value)") - case .one: - return String(format: self._ForwardedFiles_one, "\(value)") - case .two: - return String(format: self._ForwardedFiles_two, "\(value)") - case .few: - return String(format: self._ForwardedFiles_few, "\(value)") - case .many: - return String(format: self._ForwardedFiles_many, "\(value)") - case .other: - return String(format: self._ForwardedFiles_other, "\(value)") - } - } - private let _ForwardedStickers_zero: String - private let _ForwardedStickers_one: String - private let _ForwardedStickers_two: String - private let _ForwardedStickers_few: String - private let _ForwardedStickers_many: String - private let _ForwardedStickers_other: String - public func ForwardedStickers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedStickers_zero, "\(value)") - case .one: - return String(format: self._ForwardedStickers_one, "\(value)") - case .two: - return String(format: self._ForwardedStickers_two, "\(value)") - case .few: - return String(format: self._ForwardedStickers_few, "\(value)") - case .many: - return String(format: self._ForwardedStickers_many, "\(value)") - case .other: - return String(format: self._ForwardedStickers_other, "\(value)") - } - } - private let _StickerPack_StickerCount_zero: String - private let _StickerPack_StickerCount_one: String - private let _StickerPack_StickerCount_two: String - private let _StickerPack_StickerCount_few: String - private let _StickerPack_StickerCount_many: String - private let _StickerPack_StickerCount_other: String - public func StickerPack_StickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_StickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_StickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_StickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_StickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_StickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_StickerCount_other, "\(value)") - } - } - private let _ForwardedAuthorsOthers_zero: String - private let _ForwardedAuthorsOthers_one: String - private let _ForwardedAuthorsOthers_two: String - private let _ForwardedAuthorsOthers_few: String - private let _ForwardedAuthorsOthers_many: String - private let _ForwardedAuthorsOthers_other: String - public func ForwardedAuthorsOthers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedAuthorsOthers_zero, "\(value)") - case .one: - return String(format: self._ForwardedAuthorsOthers_one, "\(value)") - case .two: - return String(format: self._ForwardedAuthorsOthers_two, "\(value)") - case .few: - return String(format: self._ForwardedAuthorsOthers_few, "\(value)") - case .many: - return String(format: self._ForwardedAuthorsOthers_many, "\(value)") - case .other: - 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 _AttachmentMenu_SendVideo_zero: String - private let _AttachmentMenu_SendVideo_one: String - private let _AttachmentMenu_SendVideo_two: String - private let _AttachmentMenu_SendVideo_few: String - private let _AttachmentMenu_SendVideo_many: String - private let _AttachmentMenu_SendVideo_other: String - public func AttachmentMenu_SendVideo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendVideo_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") - } - } - private let _Call_Minutes_zero: String - private let _Call_Minutes_one: String - private let _Call_Minutes_two: String - private let _Call_Minutes_few: String - private let _Call_Minutes_many: String - private let _Call_Minutes_other: String - public func Call_Minutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_Minutes_zero, "\(value)") - case .one: - return String(format: self._Call_Minutes_one, "\(value)") - case .two: - return String(format: self._Call_Minutes_two, "\(value)") - case .few: - return String(format: self._Call_Minutes_few, "\(value)") - case .many: - return String(format: self._Call_Minutes_many, "\(value)") - case .other: - return String(format: self._Call_Minutes_other, "\(value)") - } - } - private let _ForwardedContacts_zero: String - private let _ForwardedContacts_one: String - private let _ForwardedContacts_two: String - private let _ForwardedContacts_few: String - private let _ForwardedContacts_many: String - private let _ForwardedContacts_other: String - public func ForwardedContacts(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedContacts_zero, "\(value)") - case .one: - return String(format: self._ForwardedContacts_one, "\(value)") - case .two: - return String(format: self._ForwardedContacts_two, "\(value)") - case .few: - return String(format: self._ForwardedContacts_few, "\(value)") - case .many: - return String(format: self._ForwardedContacts_many, "\(value)") - case .other: - return String(format: self._ForwardedContacts_other, "\(value)") - } - } - private let _ForwardedGifs_zero: String - private let _ForwardedGifs_one: String - private let _ForwardedGifs_two: String - private let _ForwardedGifs_few: String - private let _ForwardedGifs_many: String - private let _ForwardedGifs_other: String - public func ForwardedGifs(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedGifs_zero, "\(value)") - case .one: - return String(format: self._ForwardedGifs_one, "\(value)") - case .two: - return String(format: self._ForwardedGifs_two, "\(value)") - case .few: - return String(format: self._ForwardedGifs_few, "\(value)") - case .many: - return String(format: self._ForwardedGifs_many, "\(value)") - case .other: - 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 - private let _MessageTimer_ShortHours_few: String - private let _MessageTimer_ShortHours_many: String - private let _MessageTimer_ShortHours_other: String - public func MessageTimer_ShortHours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortHours_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortHours_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortHours_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortHours_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortHours_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortHours_other, "\(value)") - } - } - private let _ServiceMessage_GameScoreExtended_zero: String - private let _ServiceMessage_GameScoreExtended_one: String - private let _ServiceMessage_GameScoreExtended_two: String - private let _ServiceMessage_GameScoreExtended_few: String - private let _ServiceMessage_GameScoreExtended_many: String - private let _ServiceMessage_GameScoreExtended_other: String - public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") - } - } - private let _StickerPack_AddStickerCount_zero: String - private let _StickerPack_AddStickerCount_one: String - private let _StickerPack_AddStickerCount_two: String - private let _StickerPack_AddStickerCount_few: String - private let _StickerPack_AddStickerCount_many: String - private let _StickerPack_AddStickerCount_other: String - public func StickerPack_AddStickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_AddStickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_AddStickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_AddStickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_AddStickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_AddStickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_AddStickerCount_other, "\(value)") - } - } - private let _AttachmentMenu_SendPhoto_zero: String - private let _AttachmentMenu_SendPhoto_one: String - private let _AttachmentMenu_SendPhoto_two: String - private let _AttachmentMenu_SendPhoto_few: String - private let _AttachmentMenu_SendPhoto_many: String - private let _AttachmentMenu_SendPhoto_other: String - public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendPhoto_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendPhoto_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendPhoto_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendPhoto_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendPhoto_many, "\(value)") - case .other: - 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 _ServiceMessage_GameScoreSelfSimple_zero: String - private let _ServiceMessage_GameScoreSelfSimple_one: String - private let _ServiceMessage_GameScoreSelfSimple_two: String - private let _ServiceMessage_GameScoreSelfSimple_few: String - private let _ServiceMessage_GameScoreSelfSimple_many: String - private let _ServiceMessage_GameScoreSelfSimple_other: String - public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreSelfSimple_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreSelfSimple_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreSelfSimple_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreSelfSimple_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreSelfSimple_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreSelfSimple_other, "\(value)") - } - } - private let _SharedMedia_Photo_zero: String - private let _SharedMedia_Photo_one: String - private let _SharedMedia_Photo_two: String - private let _SharedMedia_Photo_few: String - private let _SharedMedia_Photo_many: String - private let _SharedMedia_Photo_other: String - public func SharedMedia_Photo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Photo_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Photo_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Photo_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Photo_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Photo_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Photo_other, "\(value)") - } - } - private let _MessageTimer_Weeks_zero: String - private let _MessageTimer_Weeks_one: String - private let _MessageTimer_Weeks_two: String - private let _MessageTimer_Weeks_few: String - private let _MessageTimer_Weeks_many: String - private let _MessageTimer_Weeks_other: String - public func MessageTimer_Weeks(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Weeks_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Weeks_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Weeks_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Weeks_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Weeks_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Weeks_other, "\(value)") - } - } - private let _StickerPack_AddMaskCount_zero: String - private let _StickerPack_AddMaskCount_one: String - private let _StickerPack_AddMaskCount_two: String - private let _StickerPack_AddMaskCount_few: String - private let _StickerPack_AddMaskCount_many: String - private let _StickerPack_AddMaskCount_other: String - public func StickerPack_AddMaskCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_AddMaskCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_AddMaskCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_AddMaskCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_AddMaskCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_AddMaskCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_AddMaskCount_other, "\(value)") - } - } - private let _MuteExpires_Days_zero: String - private let _MuteExpires_Days_one: String - private let _MuteExpires_Days_two: String - private let _MuteExpires_Days_few: String - private let _MuteExpires_Days_many: String - private let _MuteExpires_Days_other: String - public func MuteExpires_Days(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteExpires_Days_zero, "\(value)") - case .one: - return String(format: self._MuteExpires_Days_one, "\(value)") - case .two: - return String(format: self._MuteExpires_Days_two, "\(value)") - case .few: - return String(format: self._MuteExpires_Days_few, "\(value)") - case .many: - return String(format: self._MuteExpires_Days_many, "\(value)") - case .other: - return String(format: self._MuteExpires_Days_other, "\(value)") - } - } - private let _LastSeen_HoursAgo_zero: String - private let _LastSeen_HoursAgo_one: String - private let _LastSeen_HoursAgo_two: String - private let _LastSeen_HoursAgo_few: String - private let _LastSeen_HoursAgo_many: String - private let _LastSeen_HoursAgo_other: String - public func LastSeen_HoursAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LastSeen_HoursAgo_zero, "\(value)") - case .one: - return String(format: self._LastSeen_HoursAgo_one, "\(value)") - case .two: - return String(format: self._LastSeen_HoursAgo_two, "\(value)") - case .few: - return String(format: self._LastSeen_HoursAgo_few, "\(value)") - case .many: - return String(format: self._LastSeen_HoursAgo_many, "\(value)") - case .other: - return String(format: self._LastSeen_HoursAgo_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 { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Hours_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Hours_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Hours_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Hours_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Hours_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Hours_other, "\(value)") - } - } - private let _MuteExpires_Hours_zero: String - private let _MuteExpires_Hours_one: String - private let _MuteExpires_Hours_two: String - private let _MuteExpires_Hours_few: String - private let _MuteExpires_Hours_many: String - private let _MuteExpires_Hours_other: String - public func MuteExpires_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteExpires_Hours_zero, "\(value)") - case .one: - return String(format: self._MuteExpires_Hours_one, "\(value)") - case .two: - return String(format: self._MuteExpires_Hours_two, "\(value)") - case .few: - return String(format: self._MuteExpires_Hours_few, "\(value)") - case .many: - return String(format: self._MuteExpires_Hours_many, "\(value)") - case .other: - return String(format: self._MuteExpires_Hours_other, "\(value)") - } - } - private let _Watch_LastSeen_HoursAgo_zero: String - private let _Watch_LastSeen_HoursAgo_one: String - private let _Watch_LastSeen_HoursAgo_two: String - private let _Watch_LastSeen_HoursAgo_few: String - private let _Watch_LastSeen_HoursAgo_many: String - private let _Watch_LastSeen_HoursAgo_other: String - public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Watch_LastSeen_HoursAgo_zero, "\(value)") - case .one: - return String(format: self._Watch_LastSeen_HoursAgo_one, "\(value)") - case .two: - return String(format: self._Watch_LastSeen_HoursAgo_two, "\(value)") - case .few: - return String(format: self._Watch_LastSeen_HoursAgo_few, "\(value)") - case .many: - return String(format: self._Watch_LastSeen_HoursAgo_many, "\(value)") - case .other: - return String(format: self._Watch_LastSeen_HoursAgo_other, "\(value)") - } - } - private let _Forward_ConfirmMultipleFiles_zero: String - private let _Forward_ConfirmMultipleFiles_one: String - private let _Forward_ConfirmMultipleFiles_two: String - private let _Forward_ConfirmMultipleFiles_few: String - private let _Forward_ConfirmMultipleFiles_many: String - private let _Forward_ConfirmMultipleFiles_other: String - public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") - case .one: - return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") - case .two: - return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") - case .few: - return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") - case .many: - return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") - case .other: - return String(format: self._Forward_ConfirmMultipleFiles_other, "\(value)") - } - } - private let _AttachmentMenu_SendGif_zero: String - private let _AttachmentMenu_SendGif_one: String - private let _AttachmentMenu_SendGif_two: String - private let _AttachmentMenu_SendGif_few: String - private let _AttachmentMenu_SendGif_many: String - private let _AttachmentMenu_SendGif_other: String - public func AttachmentMenu_SendGif(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendGif_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendGif_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendGif_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendGif_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendGif_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendGif_other, "\(value)") - } - } - private let _StickerPack_RemoveStickerCount_zero: String - private let _StickerPack_RemoveStickerCount_one: String - private let _StickerPack_RemoveStickerCount_two: String - private let _StickerPack_RemoveStickerCount_few: String - private let _StickerPack_RemoveStickerCount_many: String - private let _StickerPack_RemoveStickerCount_other: String - public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") - } - } - private let _SharedMedia_Link_zero: String - private let _SharedMedia_Link_one: String - private let _SharedMedia_Link_two: String - private let _SharedMedia_Link_few: String - private let _SharedMedia_Link_many: String - private let _SharedMedia_Link_other: String - public func SharedMedia_Link(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Link_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Link_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Link_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Link_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Link_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Link_other, "\(value)") - } - } - private let _DialogList_LiveLocationChatsCount_zero: String - private let _DialogList_LiveLocationChatsCount_one: String - private let _DialogList_LiveLocationChatsCount_two: String - private let _DialogList_LiveLocationChatsCount_few: String - private let _DialogList_LiveLocationChatsCount_many: String - private let _DialogList_LiveLocationChatsCount_other: String - public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") - case .one: - return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") - case .two: - return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") - case .few: - return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") - case .many: - return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") - case .other: - return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") - } - } - private let _SharedMedia_DeleteItemsConfirmation_zero: String - private let _SharedMedia_DeleteItemsConfirmation_one: String - private let _SharedMedia_DeleteItemsConfirmation_two: String - private let _SharedMedia_DeleteItemsConfirmation_few: String - private let _SharedMedia_DeleteItemsConfirmation_many: String - private let _SharedMedia_DeleteItemsConfirmation_other: String - public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") - case .two: - return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") - case .few: - return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") - case .many: - return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") - case .other: - return String(format: self._SharedMedia_DeleteItemsConfirmation_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 { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedVideos_zero, "\(value)") - case .one: - return String(format: self._ForwardedVideos_one, "\(value)") - case .two: - return String(format: self._ForwardedVideos_two, "\(value)") - case .few: - return String(format: self._ForwardedVideos_few, "\(value)") - case .many: - return String(format: self._ForwardedVideos_many, "\(value)") - case .other: - return String(format: self._ForwardedVideos_other, "\(value)") - } - } - private let _ForwardedMessages_zero: String - private let _ForwardedMessages_one: String - private let _ForwardedMessages_two: String - private let _ForwardedMessages_few: String - private let _ForwardedMessages_many: String - private let _ForwardedMessages_other: String - public func ForwardedMessages(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedMessages_zero, "\(value)") - case .one: - return String(format: self._ForwardedMessages_one, "\(value)") - case .two: - return String(format: self._ForwardedMessages_two, "\(value)") - case .few: - return String(format: self._ForwardedMessages_few, "\(value)") - case .many: - return String(format: self._ForwardedMessages_many, "\(value)") - case .other: - return String(format: self._ForwardedMessages_other, "\(value)") - } - } - private let _Map_ETAHours_zero: String - private let _Map_ETAHours_one: String - private let _Map_ETAHours_two: String - private let _Map_ETAHours_few: String - private let _Map_ETAHours_many: String - private let _Map_ETAHours_other: String - public func Map_ETAHours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Map_ETAHours_zero, "\(value)") - case .one: - return String(format: self._Map_ETAHours_one, "\(value)") - case .two: - return String(format: self._Map_ETAHours_two, "\(value)") - case .few: - return String(format: self._Map_ETAHours_few, "\(value)") - case .many: - return String(format: self._Map_ETAHours_many, "\(value)") - case .other: - return String(format: self._Map_ETAHours_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 { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") - case .one: - return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") - case .two: - return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") - case .few: - return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") - case .many: - return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") - case .other: - return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") - } - } - private let _MessageTimer_Years_zero: String - private let _MessageTimer_Years_one: String - private let _MessageTimer_Years_two: String - private let _MessageTimer_Years_few: String - private let _MessageTimer_Years_many: String - private let _MessageTimer_Years_other: String - public func MessageTimer_Years(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Years_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Years_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Years_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Years_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Years_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Years_other, "\(value)") - } - } - private let _Map_ETAMinutes_zero: String - private let _Map_ETAMinutes_one: String - private let _Map_ETAMinutes_two: String - private let _Map_ETAMinutes_few: String - private let _Map_ETAMinutes_many: String - private let _Map_ETAMinutes_other: String - public func Map_ETAMinutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Map_ETAMinutes_zero, "\(value)") - case .one: - return String(format: self._Map_ETAMinutes_one, "\(value)") - case .two: - return String(format: self._Map_ETAMinutes_two, "\(value)") - case .few: - return String(format: self._Map_ETAMinutes_few, "\(value)") - case .many: - return String(format: self._Map_ETAMinutes_many, "\(value)") - case .other: - return String(format: self._Map_ETAMinutes_other, "\(value)") - } - } - private let _Notification_GameScoreSelfSimple_zero: String - private let _Notification_GameScoreSelfSimple_one: String - private let _Notification_GameScoreSelfSimple_two: String - private let _Notification_GameScoreSelfSimple_few: String - private let _Notification_GameScoreSelfSimple_many: String - private let _Notification_GameScoreSelfSimple_other: String - public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") - } - } - private let _ServiceMessage_GameScoreSimple_zero: String - private let _ServiceMessage_GameScoreSimple_one: String - private let _ServiceMessage_GameScoreSimple_two: String - private let _ServiceMessage_GameScoreSimple_few: String - private let _ServiceMessage_GameScoreSimple_many: String - private let _ServiceMessage_GameScoreSimple_other: String - public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreSimple_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreSimple_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreSimple_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreSimple_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreSimple_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreSimple_other, "\(value)") - } - } - private let _QuickSend_Photos_zero: String - private let _QuickSend_Photos_one: String - private let _QuickSend_Photos_two: String - private let _QuickSend_Photos_few: String - private let _QuickSend_Photos_many: String - private let _QuickSend_Photos_other: String - public func QuickSend_Photos(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._QuickSend_Photos_zero, "\(value)") - case .one: - return String(format: self._QuickSend_Photos_one, "\(value)") - case .two: - return String(format: self._QuickSend_Photos_two, "\(value)") - case .few: - return String(format: self._QuickSend_Photos_few, "\(value)") - case .many: - return String(format: self._QuickSend_Photos_many, "\(value)") - case .other: - return String(format: self._QuickSend_Photos_other, "\(value)") - } - } private let _MuteFor_Days_zero: String private let _MuteFor_Days_one: String private let _MuteFor_Days_two: String @@ -4488,48 +3219,26 @@ public final class PresentationStrings { return String(format: self._MuteFor_Days_other, "\(value)") } } - private let _Conversation_StatusOnline_zero: String - private let _Conversation_StatusOnline_one: String - private let _Conversation_StatusOnline_two: String - private let _Conversation_StatusOnline_few: String - private let _Conversation_StatusOnline_many: String - private let _Conversation_StatusOnline_other: String - public func Conversation_StatusOnline(_ value: Int32) -> String { + private let _MessageTimer_Weeks_zero: String + private let _MessageTimer_Weeks_one: String + private let _MessageTimer_Weeks_two: String + private let _MessageTimer_Weeks_few: String + private let _MessageTimer_Weeks_many: String + private let _MessageTimer_Weeks_other: String + public func MessageTimer_Weeks(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Conversation_StatusOnline_zero, "\(value)") + return String(format: self._MessageTimer_Weeks_zero, "\(value)") case .one: - return String(format: self._Conversation_StatusOnline_one, "\(value)") + return String(format: self._MessageTimer_Weeks_one, "\(value)") case .two: - return String(format: self._Conversation_StatusOnline_two, "\(value)") + return String(format: self._MessageTimer_Weeks_two, "\(value)") case .few: - return String(format: self._Conversation_StatusOnline_few, "\(value)") + return String(format: self._MessageTimer_Weeks_few, "\(value)") case .many: - return String(format: self._Conversation_StatusOnline_many, "\(value)") + return String(format: self._MessageTimer_Weeks_many, "\(value)") case .other: - return String(format: self._Conversation_StatusOnline_other, "\(value)") - } - } - private let _AttachmentMenu_SendItem_zero: String - private let _AttachmentMenu_SendItem_one: String - private let _AttachmentMenu_SendItem_two: String - private let _AttachmentMenu_SendItem_few: String - private let _AttachmentMenu_SendItem_many: String - private let _AttachmentMenu_SendItem_other: String - public func AttachmentMenu_SendItem(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendItem_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendItem_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendItem_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendItem_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendItem_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendItem_other, "\(value)") + return String(format: self._MessageTimer_Weeks_other, "\(value)") } } private let _Contacts_ImportersCount_zero: String @@ -4554,114 +3263,92 @@ public final class PresentationStrings { return String(format: self._Contacts_ImportersCount_other, "\(value)") } } - private let _Watch_UserInfo_Mute_zero: String - private let _Watch_UserInfo_Mute_one: String - private let _Watch_UserInfo_Mute_two: String - private let _Watch_UserInfo_Mute_few: String - private let _Watch_UserInfo_Mute_many: String - private let _Watch_UserInfo_Mute_other: String - public func Watch_UserInfo_Mute(_ 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_UserInfo_Mute_zero, "\(value)") + return String(format: self._ForwardedVideos_zero, "\(value)") case .one: - return String(format: self._Watch_UserInfo_Mute_one, "\(value)") + return String(format: self._ForwardedVideos_one, "\(value)") case .two: - return String(format: self._Watch_UserInfo_Mute_two, "\(value)") + return String(format: self._ForwardedVideos_two, "\(value)") case .few: - return String(format: self._Watch_UserInfo_Mute_few, "\(value)") + return String(format: self._ForwardedVideos_few, "\(value)") case .many: - return String(format: self._Watch_UserInfo_Mute_many, "\(value)") + return String(format: self._ForwardedVideos_many, "\(value)") case .other: - return String(format: self._Watch_UserInfo_Mute_other, "\(value)") + return String(format: self._ForwardedVideos_other, "\(value)") } } - private let _LiveLocationUpdated_MinutesAgo_zero: String - private let _LiveLocationUpdated_MinutesAgo_one: String - private let _LiveLocationUpdated_MinutesAgo_two: String - private let _LiveLocationUpdated_MinutesAgo_few: String - private let _LiveLocationUpdated_MinutesAgo_many: String - private let _LiveLocationUpdated_MinutesAgo_other: String - public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { + private let _ForwardedAuthorsOthers_zero: String + private let _ForwardedAuthorsOthers_one: String + private let _ForwardedAuthorsOthers_two: String + private let _ForwardedAuthorsOthers_few: String + private let _ForwardedAuthorsOthers_many: String + private let _ForwardedAuthorsOthers_other: String + public func ForwardedAuthorsOthers(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._LiveLocationUpdated_MinutesAgo_zero, "\(value)") + return String(format: self._ForwardedAuthorsOthers_zero, "\(value)") case .one: - return String(format: self._LiveLocationUpdated_MinutesAgo_one, "\(value)") + return String(format: self._ForwardedAuthorsOthers_one, "\(value)") case .two: - return String(format: self._LiveLocationUpdated_MinutesAgo_two, "\(value)") + return String(format: self._ForwardedAuthorsOthers_two, "\(value)") case .few: - return String(format: self._LiveLocationUpdated_MinutesAgo_few, "\(value)") + return String(format: self._ForwardedAuthorsOthers_few, "\(value)") case .many: - return String(format: self._LiveLocationUpdated_MinutesAgo_many, "\(value)") + return String(format: self._ForwardedAuthorsOthers_many, "\(value)") case .other: - return String(format: self._LiveLocationUpdated_MinutesAgo_other, "\(value)") + return String(format: self._ForwardedAuthorsOthers_other, "\(value)") } } - private let _Call_ShortMinutes_zero: String - private let _Call_ShortMinutes_one: String - private let _Call_ShortMinutes_two: String - private let _Call_ShortMinutes_few: String - private let _Call_ShortMinutes_many: String - private let _Call_ShortMinutes_other: String - public func Call_ShortMinutes(_ value: Int32) -> String { + private let _SharedMedia_Generic_zero: String + private let _SharedMedia_Generic_one: String + private let _SharedMedia_Generic_two: String + private let _SharedMedia_Generic_few: String + private let _SharedMedia_Generic_many: String + private let _SharedMedia_Generic_other: String + public func SharedMedia_Generic(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Call_ShortMinutes_zero, "\(value)") + return String(format: self._SharedMedia_Generic_zero, "\(value)") case .one: - return String(format: self._Call_ShortMinutes_one, "\(value)") + return String(format: self._SharedMedia_Generic_one, "\(value)") case .two: - return String(format: self._Call_ShortMinutes_two, "\(value)") + return String(format: self._SharedMedia_Generic_two, "\(value)") case .few: - return String(format: self._Call_ShortMinutes_few, "\(value)") + return String(format: self._SharedMedia_Generic_few, "\(value)") case .many: - return String(format: self._Call_ShortMinutes_many, "\(value)") + return String(format: self._SharedMedia_Generic_many, "\(value)") case .other: - return String(format: self._Call_ShortMinutes_other, "\(value)") + return String(format: self._SharedMedia_Generic_other, "\(value)") } } - private let _StickerPack_RemoveMaskCount_zero: String - private let _StickerPack_RemoveMaskCount_one: String - private let _StickerPack_RemoveMaskCount_two: String - private let _StickerPack_RemoveMaskCount_few: String - private let _StickerPack_RemoveMaskCount_many: String - private let _StickerPack_RemoveMaskCount_other: String - public func StickerPack_RemoveMaskCount(_ 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._StickerPack_RemoveMaskCount_zero, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") case .one: - return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") case .two: - return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") case .few: - return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") case .many: - return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") case .other: - return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") - } - } - private let _Media_ShareItem_zero: String - private let _Media_ShareItem_one: String - private let _Media_ShareItem_two: String - private let _Media_ShareItem_few: String - private let _Media_ShareItem_many: String - private let _Media_ShareItem_other: String - public func Media_ShareItem(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Media_ShareItem_zero, "\(value)") - case .one: - return String(format: self._Media_ShareItem_one, "\(value)") - case .two: - return String(format: self._Media_ShareItem_two, "\(value)") - case .few: - return String(format: self._Media_ShareItem_few, "\(value)") - case .many: - return String(format: self._Media_ShareItem_many, "\(value)") - case .other: - return String(format: self._Media_ShareItem_other, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") } } private let _ForwardedLocations_zero: String @@ -4686,6 +3373,116 @@ public final class PresentationStrings { return String(format: self._ForwardedLocations_other, "\(value)") } } + private let _Invitation_Members_zero: String + private let _Invitation_Members_one: String + private let _Invitation_Members_two: String + private let _Invitation_Members_few: String + private let _Invitation_Members_many: String + private let _Invitation_Members_other: String + public func Invitation_Members(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Invitation_Members_zero, "\(value)") + case .one: + return String(format: self._Invitation_Members_one, "\(value)") + case .two: + return String(format: self._Invitation_Members_two, "\(value)") + case .few: + return String(format: self._Invitation_Members_few, "\(value)") + case .many: + return String(format: self._Invitation_Members_many, "\(value)") + case .other: + return String(format: self._Invitation_Members_other, "\(value)") + } + } + private let _DialogList_LiveLocationChatsCount_zero: String + private let _DialogList_LiveLocationChatsCount_one: String + private let _DialogList_LiveLocationChatsCount_two: String + private let _DialogList_LiveLocationChatsCount_few: String + private let _DialogList_LiveLocationChatsCount_many: String + private let _DialogList_LiveLocationChatsCount_other: String + public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") + case .one: + return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") + case .two: + return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") + case .few: + return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") + case .many: + return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") + case .other: + return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfSimple_zero: String + private let _ServiceMessage_GameScoreSelfSimple_one: String + private let _ServiceMessage_GameScoreSelfSimple_two: String + private let _ServiceMessage_GameScoreSelfSimple_few: String + private let _ServiceMessage_GameScoreSelfSimple_many: String + private let _ServiceMessage_GameScoreSelfSimple_other: String + public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfSimple_other, "\(value)") + } + } + private let _Map_ETAMinutes_zero: String + private let _Map_ETAMinutes_one: String + private let _Map_ETAMinutes_two: String + private let _Map_ETAMinutes_few: String + private let _Map_ETAMinutes_many: String + private let _Map_ETAMinutes_other: String + public func Map_ETAMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAMinutes_zero, "\(value)") + case .one: + return String(format: self._Map_ETAMinutes_one, "\(value)") + case .two: + return String(format: self._Map_ETAMinutes_two, "\(value)") + case .few: + return String(format: self._Map_ETAMinutes_few, "\(value)") + case .many: + return String(format: self._Map_ETAMinutes_many, "\(value)") + case .other: + return String(format: self._Map_ETAMinutes_other, "\(value)") + } + } + private let _LastSeen_HoursAgo_zero: String + private let _LastSeen_HoursAgo_one: String + private let _LastSeen_HoursAgo_two: String + private let _LastSeen_HoursAgo_few: String + private let _LastSeen_HoursAgo_many: String + private let _LastSeen_HoursAgo_other: String + public func LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_HoursAgo_other, "\(value)") + } + } private let _MessageTimer_Minutes_zero: String private let _MessageTimer_Minutes_one: String private let _MessageTimer_Minutes_two: String @@ -4708,26 +3505,422 @@ 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 { + private let _LiveLocationUpdated_MinutesAgo_zero: String + private let _LiveLocationUpdated_MinutesAgo_one: String + private let _LiveLocationUpdated_MinutesAgo_two: String + private let _LiveLocationUpdated_MinutesAgo_few: String + private let _LiveLocationUpdated_MinutesAgo_many: String + private let _LiveLocationUpdated_MinutesAgo_other: String + public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_zero, "\(value)") case .one: - return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_one, "\(value)") case .two: - return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_two, "\(value)") case .few: - return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_few, "\(value)") case .many: - return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_many, "\(value)") case .other: - return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") + return String(format: self._LiveLocationUpdated_MinutesAgo_other, "\(value)") + } + } + private let _ForwardedStickers_zero: String + private let _ForwardedStickers_one: String + private let _ForwardedStickers_two: String + private let _ForwardedStickers_few: String + private let _ForwardedStickers_many: String + private let _ForwardedStickers_other: String + public func ForwardedStickers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedStickers_zero, "\(value)") + case .one: + return String(format: self._ForwardedStickers_one, "\(value)") + case .two: + return String(format: self._ForwardedStickers_two, "\(value)") + case .few: + return String(format: self._ForwardedStickers_few, "\(value)") + case .many: + return String(format: self._ForwardedStickers_many, "\(value)") + case .other: + return String(format: self._ForwardedStickers_other, "\(value)") + } + } + private let _GroupInfo_ParticipantCount_zero: String + private let _GroupInfo_ParticipantCount_one: String + private let _GroupInfo_ParticipantCount_two: String + private let _GroupInfo_ParticipantCount_few: String + private let _GroupInfo_ParticipantCount_many: String + private let _GroupInfo_ParticipantCount_other: String + public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._GroupInfo_ParticipantCount_zero, "\(value)") + case .one: + return String(format: self._GroupInfo_ParticipantCount_one, "\(value)") + case .two: + return String(format: self._GroupInfo_ParticipantCount_two, "\(value)") + case .few: + return String(format: self._GroupInfo_ParticipantCount_few, "\(value)") + case .many: + return String(format: self._GroupInfo_ParticipantCount_many, "\(value)") + case .other: + return String(format: self._GroupInfo_ParticipantCount_other, "\(value)") + } + } + private let _SharedMedia_DeleteItemsConfirmation_zero: String + private let _SharedMedia_DeleteItemsConfirmation_one: String + private let _SharedMedia_DeleteItemsConfirmation_two: String + private let _SharedMedia_DeleteItemsConfirmation_few: String + private let _SharedMedia_DeleteItemsConfirmation_many: String + private let _SharedMedia_DeleteItemsConfirmation_other: String + public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") + case .two: + return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") + case .few: + return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") + case .many: + return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") + case .other: + return String(format: self._SharedMedia_DeleteItemsConfirmation_other, "\(value)") + } + } + private let _Forward_ConfirmMultipleFiles_zero: String + private let _Forward_ConfirmMultipleFiles_one: String + private let _Forward_ConfirmMultipleFiles_two: String + private let _Forward_ConfirmMultipleFiles_few: String + private let _Forward_ConfirmMultipleFiles_many: String + private let _Forward_ConfirmMultipleFiles_other: String + public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") + case .one: + return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") + case .two: + return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") + case .few: + return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") + case .many: + return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") + case .other: + return String(format: self._Forward_ConfirmMultipleFiles_other, "\(value)") + } + } + private let _Call_Seconds_zero: String + private let _Call_Seconds_one: String + private let _Call_Seconds_two: String + private let _Call_Seconds_few: String + private let _Call_Seconds_many: String + private let _Call_Seconds_other: String + public func Call_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_Seconds_zero, "\(value)") + case .one: + return String(format: self._Call_Seconds_one, "\(value)") + case .two: + return String(format: self._Call_Seconds_two, "\(value)") + case .few: + return String(format: self._Call_Seconds_few, "\(value)") + case .many: + return String(format: self._Call_Seconds_many, "\(value)") + case .other: + return String(format: self._Call_Seconds_other, "\(value)") + } + } + private let _PasscodeSettings_FailedAttempts_zero: String + private let _PasscodeSettings_FailedAttempts_one: String + private let _PasscodeSettings_FailedAttempts_two: String + private let _PasscodeSettings_FailedAttempts_few: String + private let _PasscodeSettings_FailedAttempts_many: String + private let _PasscodeSettings_FailedAttempts_other: String + public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PasscodeSettings_FailedAttempts_zero, "\(value)") + case .one: + return String(format: self._PasscodeSettings_FailedAttempts_one, "\(value)") + case .two: + return String(format: self._PasscodeSettings_FailedAttempts_two, "\(value)") + case .few: + return String(format: self._PasscodeSettings_FailedAttempts_few, "\(value)") + case .many: + return String(format: self._PasscodeSettings_FailedAttempts_many, "\(value)") + case .other: + return String(format: self._PasscodeSettings_FailedAttempts_other, "\(value)") + } + } + private let _ForwardedFiles_zero: String + private let _ForwardedFiles_one: String + private let _ForwardedFiles_two: String + private let _ForwardedFiles_few: String + private let _ForwardedFiles_many: String + private let _ForwardedFiles_other: String + public func ForwardedFiles(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedFiles_zero, "\(value)") + case .one: + return String(format: self._ForwardedFiles_one, "\(value)") + case .two: + return String(format: self._ForwardedFiles_two, "\(value)") + case .few: + return String(format: self._ForwardedFiles_few, "\(value)") + case .many: + return String(format: self._ForwardedFiles_many, "\(value)") + case .other: + return String(format: self._ForwardedFiles_other, "\(value)") + } + } + private let _MuteFor_Hours_zero: String + private let _MuteFor_Hours_one: String + private let _MuteFor_Hours_two: String + private let _MuteFor_Hours_few: String + private let _MuteFor_Hours_many: String + private let _MuteFor_Hours_other: String + public func MuteFor_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Hours_one, "\(value)") + case .two: + return String(format: self._MuteFor_Hours_two, "\(value)") + case .few: + return String(format: self._MuteFor_Hours_few, "\(value)") + case .many: + return String(format: self._MuteFor_Hours_many, "\(value)") + case .other: + return String(format: self._MuteFor_Hours_other, "\(value)") + } + } + private let _AttachmentMenu_SendGif_zero: String + private let _AttachmentMenu_SendGif_one: String + private let _AttachmentMenu_SendGif_two: String + private let _AttachmentMenu_SendGif_few: String + private let _AttachmentMenu_SendGif_many: String + private let _AttachmentMenu_SendGif_other: String + public func AttachmentMenu_SendGif(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendGif_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendGif_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendGif_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendGif_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendGif_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendGif_other, "\(value)") + } + } + private let _Conversation_StatusMembers_zero: String + private let _Conversation_StatusMembers_one: String + private let _Conversation_StatusMembers_two: String + private let _Conversation_StatusMembers_few: String + private let _Conversation_StatusMembers_many: String + private let _Conversation_StatusMembers_other: String + public func Conversation_StatusMembers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusMembers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusMembers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusMembers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusMembers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusMembers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusMembers_other, "\(value)") + } + } + private let _LiveLocation_MenuChatsCount_zero: String + private let _LiveLocation_MenuChatsCount_one: String + private let _LiveLocation_MenuChatsCount_two: String + private let _LiveLocation_MenuChatsCount_few: String + private let _LiveLocation_MenuChatsCount_many: String + private let _LiveLocation_MenuChatsCount_other: String + public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LiveLocation_MenuChatsCount_zero, "\(value)") + case .one: + return String(format: self._LiveLocation_MenuChatsCount_one, "\(value)") + case .two: + return String(format: self._LiveLocation_MenuChatsCount_two, "\(value)") + case .few: + return String(format: self._LiveLocation_MenuChatsCount_few, "\(value)") + case .many: + return String(format: self._LiveLocation_MenuChatsCount_many, "\(value)") + case .other: + return String(format: self._LiveLocation_MenuChatsCount_other, "\(value)") + } + } + private let _ForwardedGifs_zero: String + private let _ForwardedGifs_one: String + private let _ForwardedGifs_two: String + private let _ForwardedGifs_few: String + private let _ForwardedGifs_many: String + private let _ForwardedGifs_other: String + public func ForwardedGifs(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedGifs_zero, "\(value)") + case .one: + return String(format: self._ForwardedGifs_one, "\(value)") + case .two: + return String(format: self._ForwardedGifs_two, "\(value)") + case .few: + return String(format: self._ForwardedGifs_few, "\(value)") + case .many: + return String(format: self._ForwardedGifs_many, "\(value)") + case .other: + return String(format: self._ForwardedGifs_other, "\(value)") + } + } + private let _MuteExpires_Days_zero: String + private let _MuteExpires_Days_one: String + private let _MuteExpires_Days_two: String + private let _MuteExpires_Days_few: String + private let _MuteExpires_Days_many: String + private let _MuteExpires_Days_other: String + public func MuteExpires_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Days_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Days_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Days_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Days_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Days_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Days_other, "\(value)") + } + } + private let _MessageTimer_Years_zero: String + private let _MessageTimer_Years_one: String + private let _MessageTimer_Years_two: String + private let _MessageTimer_Years_few: String + private let _MessageTimer_Years_many: String + private let _MessageTimer_Years_other: String + public func MessageTimer_Years(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Years_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Years_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Years_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Years_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Years_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Years_other, "\(value)") + } + } + private let _MessageTimer_ShortDays_zero: String + private let _MessageTimer_ShortDays_one: String + private let _MessageTimer_ShortDays_two: String + private let _MessageTimer_ShortDays_few: String + private let _MessageTimer_ShortDays_many: String + private let _MessageTimer_ShortDays_other: String + public func MessageTimer_ShortDays(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortDays_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortDays_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortDays_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortDays_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortDays_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortDays_other, "\(value)") + } + } + private let _InviteText_ContactsCount_zero: String + private let _InviteText_ContactsCount_one: String + private let _InviteText_ContactsCount_two: String + private let _InviteText_ContactsCount_few: String + private let _InviteText_ContactsCount_many: String + private let _InviteText_ContactsCount_other: String + public func InviteText_ContactsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._InviteText_ContactsCount_zero, "\(value)") + case .one: + return String(format: self._InviteText_ContactsCount_one, "\(value)") + case .two: + return String(format: self._InviteText_ContactsCount_two, "\(value)") + case .few: + return String(format: self._InviteText_ContactsCount_few, "\(value)") + case .many: + return String(format: self._InviteText_ContactsCount_many, "\(value)") + case .other: + return String(format: self._InviteText_ContactsCount_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 _MessageTimer_Seconds_zero: String + private let _MessageTimer_Seconds_one: String + private let _MessageTimer_Seconds_two: String + private let _MessageTimer_Seconds_few: String + private let _MessageTimer_Seconds_many: String + private let _MessageTimer_Seconds_other: String + public func MessageTimer_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Seconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Seconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Seconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Seconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Seconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Seconds_other, "\(value)") } } private let _MessageTimer_Months_zero: String @@ -4752,6 +3945,534 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Months_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 { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Hours_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Hours_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Hours_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Hours_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Hours_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Hours_other, "\(value)") + } + } + private let _Call_ShortMinutes_zero: String + private let _Call_ShortMinutes_one: String + private let _Call_ShortMinutes_two: String + private let _Call_ShortMinutes_few: String + private let _Call_ShortMinutes_many: String + private let _Call_ShortMinutes_other: String + public func Call_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._Call_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._Call_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._Call_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._Call_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._Call_ShortMinutes_other, "\(value)") + } + } + private let _ForwardedPhotos_zero: String + private let _ForwardedPhotos_one: String + private let _ForwardedPhotos_two: String + private let _ForwardedPhotos_few: String + private let _ForwardedPhotos_many: String + private let _ForwardedPhotos_other: String + public func ForwardedPhotos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedPhotos_zero, "\(value)") + case .one: + return String(format: self._ForwardedPhotos_one, "\(value)") + case .two: + return String(format: self._ForwardedPhotos_two, "\(value)") + case .few: + return String(format: self._ForwardedPhotos_few, "\(value)") + case .many: + return String(format: self._ForwardedPhotos_many, "\(value)") + case .other: + return String(format: self._ForwardedPhotos_other, "\(value)") + } + } + private let _StickerPack_RemoveMaskCount_zero: String + private let _StickerPack_RemoveMaskCount_one: String + private let _StickerPack_RemoveMaskCount_two: String + private let _StickerPack_RemoveMaskCount_few: String + private let _StickerPack_RemoveMaskCount_many: String + private let _StickerPack_RemoveMaskCount_other: String + public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") + } + } + private let _Conversation_StatusSubscribers_zero: String + private let _Conversation_StatusSubscribers_one: String + private let _Conversation_StatusSubscribers_two: String + private let _Conversation_StatusSubscribers_few: String + private let _Conversation_StatusSubscribers_many: String + private let _Conversation_StatusSubscribers_other: String + public func Conversation_StatusSubscribers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusSubscribers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusSubscribers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusSubscribers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusSubscribers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusSubscribers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusSubscribers_other, "\(value)") + } + } + private let _ForwardedContacts_zero: String + private let _ForwardedContacts_one: String + private let _ForwardedContacts_two: String + private let _ForwardedContacts_few: String + private let _ForwardedContacts_many: String + private let _ForwardedContacts_other: String + public func ForwardedContacts(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedContacts_zero, "\(value)") + case .one: + return String(format: self._ForwardedContacts_one, "\(value)") + case .two: + return String(format: self._ForwardedContacts_two, "\(value)") + case .few: + return String(format: self._ForwardedContacts_few, "\(value)") + case .many: + return String(format: self._ForwardedContacts_many, "\(value)") + case .other: + return String(format: self._ForwardedContacts_other, "\(value)") + } + } + private let _Notification_GameScoreSelfSimple_zero: String + private let _Notification_GameScoreSelfSimple_one: String + private let _Notification_GameScoreSelfSimple_two: String + private let _Notification_GameScoreSelfSimple_few: String + private let _Notification_GameScoreSelfSimple_many: String + private let _Notification_GameScoreSelfSimple_other: String + public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") + } + } + private let _Watch_UserInfo_Mute_zero: String + private let _Watch_UserInfo_Mute_one: String + private let _Watch_UserInfo_Mute_two: String + private let _Watch_UserInfo_Mute_few: String + private let _Watch_UserInfo_Mute_many: String + private let _Watch_UserInfo_Mute_other: String + public func Watch_UserInfo_Mute(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_UserInfo_Mute_zero, "\(value)") + case .one: + return String(format: self._Watch_UserInfo_Mute_one, "\(value)") + case .two: + return String(format: self._Watch_UserInfo_Mute_two, "\(value)") + case .few: + return String(format: self._Watch_UserInfo_Mute_few, "\(value)") + case .many: + return String(format: self._Watch_UserInfo_Mute_many, "\(value)") + case .other: + return String(format: self._Watch_UserInfo_Mute_other, "\(value)") + } + } + private let _Media_SharePhoto_zero: String + private let _Media_SharePhoto_one: String + private let _Media_SharePhoto_two: String + private let _Media_SharePhoto_few: String + private let _Media_SharePhoto_many: String + private let _Media_SharePhoto_other: String + public func Media_SharePhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_SharePhoto_zero, "\(value)") + case .one: + return String(format: self._Media_SharePhoto_one, "\(value)") + case .two: + return String(format: self._Media_SharePhoto_two, "\(value)") + case .few: + return String(format: self._Media_SharePhoto_few, "\(value)") + case .many: + return String(format: self._Media_SharePhoto_many, "\(value)") + case .other: + return String(format: self._Media_SharePhoto_other, "\(value)") + } + } + private let _AttachmentMenu_SendVideo_zero: String + private let _AttachmentMenu_SendVideo_one: String + private let _AttachmentMenu_SendVideo_two: String + private let _AttachmentMenu_SendVideo_few: String + private let _AttachmentMenu_SendVideo_many: String + private let _AttachmentMenu_SendVideo_other: String + public func AttachmentMenu_SendVideo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendVideo_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") + } + } + private let _SharedMedia_Photo_zero: String + private let _SharedMedia_Photo_one: String + private let _SharedMedia_Photo_two: String + private let _SharedMedia_Photo_few: String + private let _SharedMedia_Photo_many: String + private let _SharedMedia_Photo_other: String + public func SharedMedia_Photo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Photo_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Photo_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Photo_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Photo_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Photo_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Photo_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 _Media_ShareItem_zero: String + private let _Media_ShareItem_one: String + private let _Media_ShareItem_two: String + private let _Media_ShareItem_few: String + private let _Media_ShareItem_many: String + private let _Media_ShareItem_other: String + public func Media_ShareItem(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_ShareItem_zero, "\(value)") + case .one: + return String(format: self._Media_ShareItem_one, "\(value)") + case .two: + return String(format: self._Media_ShareItem_two, "\(value)") + case .few: + return String(format: self._Media_ShareItem_few, "\(value)") + case .many: + return String(format: self._Media_ShareItem_many, "\(value)") + case .other: + return String(format: self._Media_ShareItem_other, "\(value)") + } + } + private let _StickerPack_RemoveStickerCount_zero: String + private let _StickerPack_RemoveStickerCount_one: String + private let _StickerPack_RemoveStickerCount_two: String + private let _StickerPack_RemoveStickerCount_few: String + private let _StickerPack_RemoveStickerCount_many: String + private let _StickerPack_RemoveStickerCount_other: String + public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") + } + } + private let _MessageTimer_ShortHours_zero: String + private let _MessageTimer_ShortHours_one: String + private let _MessageTimer_ShortHours_two: String + private let _MessageTimer_ShortHours_few: String + private let _MessageTimer_ShortHours_many: String + private let _MessageTimer_ShortHours_other: String + public func MessageTimer_ShortHours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortHours_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortHours_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortHours_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortHours_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortHours_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortHours_other, "\(value)") + } + } + private let _StickerPack_AddStickerCount_zero: String + private let _StickerPack_AddStickerCount_one: String + private let _StickerPack_AddStickerCount_two: String + private let _StickerPack_AddStickerCount_few: String + private let _StickerPack_AddStickerCount_many: String + private let _StickerPack_AddStickerCount_other: String + public func StickerPack_AddStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddStickerCount_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 _StickerPack_AddMaskCount_zero: String + private let _StickerPack_AddMaskCount_one: String + private let _StickerPack_AddMaskCount_two: String + private let _StickerPack_AddMaskCount_few: String + private let _StickerPack_AddMaskCount_many: String + private let _StickerPack_AddMaskCount_other: String + public func StickerPack_AddMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddMaskCount_other, "\(value)") + } + } + private let _ForwardedMessages_zero: String + private let _ForwardedMessages_one: String + private let _ForwardedMessages_two: String + private let _ForwardedMessages_few: String + private let _ForwardedMessages_many: String + private let _ForwardedMessages_other: String + public func ForwardedMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedMessages_many, "\(value)") + case .other: + return String(format: self._ForwardedMessages_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 _ForwardedAudios_zero: String + private let _ForwardedAudios_one: String + private let _ForwardedAudios_two: String + private let _ForwardedAudios_few: String + private let _ForwardedAudios_many: String + private let _ForwardedAudios_other: String + public func ForwardedAudios(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedAudios_zero, "\(value)") + case .one: + return String(format: self._ForwardedAudios_one, "\(value)") + case .two: + return String(format: self._ForwardedAudios_two, "\(value)") + case .few: + return String(format: self._ForwardedAudios_few, "\(value)") + case .many: + return String(format: self._ForwardedAudios_many, "\(value)") + case .other: + return String(format: self._ForwardedAudios_other, "\(value)") + } + } + private let _ForwardedVideoMessages_zero: String + private let _ForwardedVideoMessages_one: String + private let _ForwardedVideoMessages_two: String + private let _ForwardedVideoMessages_few: String + private let _ForwardedVideoMessages_many: String + private let _ForwardedVideoMessages_other: String + public func ForwardedVideoMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedVideoMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedVideoMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedVideoMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedVideoMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedVideoMessages_many, "\(value)") + case .other: + return String(format: self._ForwardedVideoMessages_other, "\(value)") + } + } + private let _Watch_LastSeen_HoursAgo_zero: String + private let _Watch_LastSeen_HoursAgo_one: String + private let _Watch_LastSeen_HoursAgo_two: String + private let _Watch_LastSeen_HoursAgo_few: String + private let _Watch_LastSeen_HoursAgo_many: String + private let _Watch_LastSeen_HoursAgo_other: String + public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._Watch_LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._Watch_LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._Watch_LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._Watch_LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._Watch_LastSeen_HoursAgo_other, "\(value)") + } + } + private let _MessageTimer_ShortMinutes_zero: String + private let _MessageTimer_ShortMinutes_one: String + private let _MessageTimer_ShortMinutes_two: String + private let _MessageTimer_ShortMinutes_few: String + private let _MessageTimer_ShortMinutes_many: String + private let _MessageTimer_ShortMinutes_other: String + public func MessageTimer_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortMinutes_other, "\(value)") + } + } private let _MessageTimer_Days_zero: String private let _MessageTimer_Days_one: String private let _MessageTimer_Days_two: String @@ -4774,6 +4495,204 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Days_other, "\(value)") } } + private let _AttachmentMenu_SendPhoto_zero: String + private let _AttachmentMenu_SendPhoto_one: String + private let _AttachmentMenu_SendPhoto_two: String + private let _AttachmentMenu_SendPhoto_few: String + private let _AttachmentMenu_SendPhoto_many: String + private let _AttachmentMenu_SendPhoto_other: String + public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendPhoto_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendPhoto_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendPhoto_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendPhoto_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendPhoto_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendPhoto_other, "\(value)") + } + } + private let _Conversation_StatusOnline_zero: String + private let _Conversation_StatusOnline_one: String + private let _Conversation_StatusOnline_two: String + private let _Conversation_StatusOnline_few: String + private let _Conversation_StatusOnline_many: String + private let _Conversation_StatusOnline_other: String + public func Conversation_StatusOnline(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusOnline_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusOnline_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusOnline_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusOnline_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusOnline_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusOnline_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfExtended_zero: String + private let _ServiceMessage_GameScoreSelfExtended_one: String + private let _ServiceMessage_GameScoreSelfExtended_two: String + private let _ServiceMessage_GameScoreSelfExtended_few: String + private let _ServiceMessage_GameScoreSelfExtended_many: String + private let _ServiceMessage_GameScoreSelfExtended_other: String + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfExtended_other, "\(value)") + } + } + private let _MuteExpires_Hours_zero: String + private let _MuteExpires_Hours_one: String + private let _MuteExpires_Hours_two: String + private let _MuteExpires_Hours_few: String + private let _MuteExpires_Hours_many: String + private let _MuteExpires_Hours_other: String + public func MuteExpires_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Hours_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Hours_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Hours_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Hours_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Hours_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreExtended_zero: String + private let _ServiceMessage_GameScoreExtended_one: String + private let _ServiceMessage_GameScoreExtended_two: String + private let _ServiceMessage_GameScoreExtended_few: String + private let _ServiceMessage_GameScoreExtended_many: String + private let _ServiceMessage_GameScoreExtended_other: String + public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSimple_zero: String + private let _ServiceMessage_GameScoreSimple_one: String + private let _ServiceMessage_GameScoreSimple_two: String + private let _ServiceMessage_GameScoreSimple_few: String + private let _ServiceMessage_GameScoreSimple_many: String + private let _ServiceMessage_GameScoreSimple_other: String + public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSimple_other, "\(value)") + } + } + private let _Conversation_LiveLocationMembersCount_zero: String + private let _Conversation_LiveLocationMembersCount_one: String + private let _Conversation_LiveLocationMembersCount_two: String + private let _Conversation_LiveLocationMembersCount_few: String + private let _Conversation_LiveLocationMembersCount_many: String + private let _Conversation_LiveLocationMembersCount_other: String + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_LiveLocationMembersCount_zero, "\(value)") + case .one: + return String(format: self._Conversation_LiveLocationMembersCount_one, "\(value)") + case .two: + return String(format: self._Conversation_LiveLocationMembersCount_two, "\(value)") + case .few: + return String(format: self._Conversation_LiveLocationMembersCount_few, "\(value)") + case .many: + return String(format: self._Conversation_LiveLocationMembersCount_many, "\(value)") + case .other: + return String(format: self._Conversation_LiveLocationMembersCount_other, "\(value)") + } + } + private let _Call_ShortSeconds_zero: String + private let _Call_ShortSeconds_one: String + private let _Call_ShortSeconds_two: String + private let _Call_ShortSeconds_few: String + private let _Call_ShortSeconds_many: String + private let _Call_ShortSeconds_other: String + public func Call_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._Call_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._Call_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._Call_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._Call_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._Call_ShortSeconds_other, "\(value)") + } + } + private let _QuickSend_Photos_zero: String + private let _QuickSend_Photos_one: String + private let _QuickSend_Photos_two: String + private let _QuickSend_Photos_few: String + private let _QuickSend_Photos_many: String + private let _QuickSend_Photos_other: String + public func QuickSend_Photos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._QuickSend_Photos_zero, "\(value)") + case .one: + return String(format: self._QuickSend_Photos_one, "\(value)") + case .two: + return String(format: self._QuickSend_Photos_two, "\(value)") + case .few: + return String(format: self._QuickSend_Photos_few, "\(value)") + case .many: + return String(format: self._QuickSend_Photos_many, "\(value)") + case .other: + return String(format: self._QuickSend_Photos_other, "\(value)") + } + } private let _MuteExpires_Minutes_zero: String private let _MuteExpires_Minutes_one: String private let _MuteExpires_Minutes_two: String @@ -4796,6 +4715,248 @@ public final class PresentationStrings { return String(format: self._MuteExpires_Minutes_other, "\(value)") } } + private let _Map_ETAHours_zero: String + private let _Map_ETAHours_one: String + private let _Map_ETAHours_two: String + private let _Map_ETAHours_few: String + private let _Map_ETAHours_many: String + private let _Map_ETAHours_other: String + public func Map_ETAHours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAHours_zero, "\(value)") + case .one: + return String(format: self._Map_ETAHours_one, "\(value)") + case .two: + return String(format: self._Map_ETAHours_two, "\(value)") + case .few: + return String(format: self._Map_ETAHours_few, "\(value)") + case .many: + return String(format: self._Map_ETAHours_many, "\(value)") + case .other: + return String(format: self._Map_ETAHours_other, "\(value)") + } + } + private let _Media_ShareVideo_zero: String + private let _Media_ShareVideo_one: String + private let _Media_ShareVideo_two: String + private let _Media_ShareVideo_few: String + private let _Media_ShareVideo_many: String + private let _Media_ShareVideo_other: String + public func Media_ShareVideo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_ShareVideo_zero, "\(value)") + case .one: + return String(format: self._Media_ShareVideo_one, "\(value)") + case .two: + return String(format: self._Media_ShareVideo_two, "\(value)") + case .few: + return String(format: self._Media_ShareVideo_few, "\(value)") + case .many: + return String(format: self._Media_ShareVideo_many, "\(value)") + case .other: + return String(format: self._Media_ShareVideo_other, "\(value)") + } + } + private let _StickerPack_StickerCount_zero: String + private let _StickerPack_StickerCount_one: String + private let _StickerPack_StickerCount_two: String + private let _StickerPack_StickerCount_few: String + private let _StickerPack_StickerCount_many: String + private let _StickerPack_StickerCount_other: String + public func StickerPack_StickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_StickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_StickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_StickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_StickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_StickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_StickerCount_other, "\(value)") + } + } + private let _AttachmentMenu_SendItem_zero: String + private let _AttachmentMenu_SendItem_one: String + private let _AttachmentMenu_SendItem_two: String + private let _AttachmentMenu_SendItem_few: String + private let _AttachmentMenu_SendItem_many: String + private let _AttachmentMenu_SendItem_other: String + public func AttachmentMenu_SendItem(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendItem_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendItem_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendItem_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendItem_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendItem_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendItem_other, "\(value)") + } + } + private let _Notification_GameScoreExtended_zero: String + private let _Notification_GameScoreExtended_one: String + private let _Notification_GameScoreExtended_two: String + private let _Notification_GameScoreExtended_few: String + private let _Notification_GameScoreExtended_many: String + private let _Notification_GameScoreExtended_other: String + public func Notification_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreExtended_other, "\(value)") + } + } + private let _Notification_GameScoreSelfExtended_zero: String + private let _Notification_GameScoreSelfExtended_one: String + private let _Notification_GameScoreSelfExtended_two: String + private let _Notification_GameScoreSelfExtended_few: String + private let _Notification_GameScoreSelfExtended_many: String + private let _Notification_GameScoreSelfExtended_other: String + public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") + } + } + private let _SharedMedia_Link_zero: String + private let _SharedMedia_Link_one: String + private let _SharedMedia_Link_two: String + private let _SharedMedia_Link_few: String + private let _SharedMedia_Link_many: String + private let _SharedMedia_Link_other: String + public func SharedMedia_Link(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Link_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Link_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Link_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Link_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Link_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Link_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 _Call_Minutes_zero: String + private let _Call_Minutes_one: String + private let _Call_Minutes_two: String + private let _Call_Minutes_few: String + private let _Call_Minutes_many: String + private let _Call_Minutes_other: String + public func Call_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_Minutes_zero, "\(value)") + case .one: + return String(format: self._Call_Minutes_one, "\(value)") + case .two: + return String(format: self._Call_Minutes_two, "\(value)") + case .few: + return String(format: self._Call_Minutes_few, "\(value)") + case .many: + return String(format: self._Call_Minutes_many, "\(value)") + case .other: + return String(format: self._Call_Minutes_other, "\(value)") + } + } + private let _MessageTimer_ShortSeconds_zero: String + private let _MessageTimer_ShortSeconds_one: String + private let _MessageTimer_ShortSeconds_two: String + private let _MessageTimer_ShortSeconds_few: String + private let _MessageTimer_ShortSeconds_many: String + private let _MessageTimer_ShortSeconds_other: String + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") + } + } + private let _SharedMedia_File_zero: String + private let _SharedMedia_File_one: String + private let _SharedMedia_File_two: String + private let _SharedMedia_File_few: String + private let _SharedMedia_File_many: String + private let _SharedMedia_File_other: String + public func SharedMedia_File(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_File_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_File_one, "\(value)") + case .two: + return String(format: self._SharedMedia_File_two, "\(value)") + case .few: + return String(format: self._SharedMedia_File_few, "\(value)") + case .many: + return String(format: self._SharedMedia_File_many, "\(value)") + case .other: + return String(format: self._SharedMedia_File_other, "\(value)") + } + } init(languageCode: String, dict: [String: String]) { @@ -4847,6 +5008,8 @@ public final class PresentationStrings { self._Call_StatusOngoing = getValue(dict, "Call.StatusOngoing") self._Call_StatusOngoing_r = extractArgumentRanges(self._Call_StatusOngoing) self.Settings_LogoutConfirmationText = getValue(dict, "Settings.LogoutConfirmationText") + self.AutoNightTheme_ScheduledTo = getValue(dict, "AutoNightTheme.ScheduledTo") + self.SocksProxySetup_RequiredCredentials = getValue(dict, "SocksProxySetup.RequiredCredentials") self.BlockedUsers_Info = getValue(dict, "BlockedUsers.Info") self.ChatSettings_AutomaticAudioDownload = getValue(dict, "ChatSettings.AutomaticAudioDownload") self.Settings_SetUsername = getValue(dict, "Settings.SetUsername") @@ -4855,6 +5018,7 @@ public final class PresentationStrings { self.Message_PinnedInvoice = getValue(dict, "Message.PinnedInvoice") self.Login_InfoAvatarAdd = getValue(dict, "Login.InfoAvatarAdd") self.Conversation_RestrictedMedia = getValue(dict, "Conversation.RestrictedMedia") + self.AutoDownloadSettings_LimitBySize = getValue(dict, "AutoDownloadSettings.LimitBySize") 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) @@ -4865,6 +5029,7 @@ public final class PresentationStrings { self._Profile_CreateEncryptedChatOutdatedError_r = extractArgumentRanges(self._Profile_CreateEncryptedChatOutdatedError) self._PINNED_STICKER = getValue(dict, "PINNED_STICKER") self._PINNED_STICKER_r = extractArgumentRanges(self._PINNED_STICKER) + self.AutoDownloadSettings_Title = getValue(dict, "AutoDownloadSettings.Title") self.Conversation_ShareInlineBotLocationConfirmation = getValue(dict, "Conversation.ShareInlineBotLocationConfirmation") self._Channel_AdminLog_MessageEdited = getValue(dict, "Channel.AdminLog.MessageEdited") self._Channel_AdminLog_MessageEdited_r = extractArgumentRanges(self._Channel_AdminLog_MessageEdited) @@ -4879,6 +5044,7 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageAdmin = getValue(dict, "Channel.AdminLog.MessageAdmin") self._Channel_AdminLog_MessageAdmin_r = extractArgumentRanges(self._Channel_AdminLog_MessageAdmin) self.PrivacyLastSeenSettings_NeverShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Placeholder") + self.Appearance_AutoNightThemeDisabled = getValue(dict, "Appearance.AutoNightThemeDisabled") self.TwoStepAuth_SetupEmail = getValue(dict, "TwoStepAuth.SetupEmail") self.Checkout_PayWithFaceId = getValue(dict, "Checkout.PayWithFaceId") self.Login_ResetAccountProtected_Reset = getValue(dict, "Login.ResetAccountProtected.Reset") @@ -4899,11 +5065,13 @@ public final class PresentationStrings { self.Paint_Delete = getValue(dict, "Paint.Delete") self.Channel_MessagePhotoUpdated = getValue(dict, "Channel.MessagePhotoUpdated") self.Cache_Help = getValue(dict, "Cache.Help") + self.SocksProxySetup_ProxyStatusConnected = getValue(dict, "SocksProxySetup.ProxyStatusConnected") self._Login_EmailPhoneBody = getValue(dict, "Login.EmailPhoneBody") self._Login_EmailPhoneBody_r = extractArgumentRanges(self._Login_EmailPhoneBody) self.Checkout_ShippingAddress = getValue(dict, "Checkout.ShippingAddress") self.Channel_BanList_RestrictedTitle = getValue(dict, "Channel.BanList.RestrictedTitle") self.Checkout_TotalAmount = getValue(dict, "Checkout.TotalAmount") + self.Appearance_TextSize = getValue(dict, "Appearance.TextSize") self.Conversation_MessageEditedLabel = getValue(dict, "Conversation.MessageEditedLabel") self.SharedMedia_EmptyLinksText = getValue(dict, "SharedMedia.EmptyLinksText") self._Conversation_RestrictedTextTimed = getValue(dict, "Conversation.RestrictedTextTimed") @@ -4925,6 +5093,7 @@ public final class PresentationStrings { self.MusicPlayer_VoiceNote = getValue(dict, "MusicPlayer.VoiceNote") self.Paint_Duplicate = getValue(dict, "Paint.Duplicate") self.Channel_Username_InvalidTaken = getValue(dict, "Channel.Username.InvalidTaken") + self.Conversation_ClearGroupHistory = getValue(dict, "Conversation.ClearGroupHistory") self.Stickers_GroupStickersHelp = getValue(dict, "Stickers.GroupStickersHelp") self.SecretChat_Title = getValue(dict, "SecretChat.Title") self.Group_UpgradeConfirmation = getValue(dict, "Group.UpgradeConfirmation") @@ -4932,6 +5101,7 @@ public final class PresentationStrings { self.GroupInfo_GroupNamePlaceholder = getValue(dict, "GroupInfo.GroupNamePlaceholder") self._Time_PreciseDate_m11 = getValue(dict, "Time.PreciseDate_m11") self._Time_PreciseDate_m11_r = extractArgumentRanges(self._Time_PreciseDate_m11) + self.TermsOfService_DeclineAuthorized = getValue(dict, "TermsOfService.DeclineAuthorized") self._MESSAGE_GEOLIVE = getValue(dict, "MESSAGE_GEOLIVE") self._MESSAGE_GEOLIVE_r = extractArgumentRanges(self._MESSAGE_GEOLIVE) self._Notification_JoinedGroupByLink = getValue(dict, "Notification.JoinedGroupByLink") @@ -4952,7 +5122,7 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageToggleSignaturesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOff) self.Month_ShortDecember = getValue(dict, "Month.ShortDecember") self.Channel_SignMessages = getValue(dict, "Channel.SignMessages") - self.ChatSettings_AutomaticDownloadVoiceMessage = getValue(dict, "ChatSettings.AutomaticDownloadVoiceMessage") + self.Appearance_Title = getValue(dict, "Appearance.Title") self.Conversation_Moderate_Delete = getValue(dict, "Conversation.Moderate.Delete") self.Conversation_CloudStorage_ChatStatus = getValue(dict, "Conversation.CloudStorage.ChatStatus") self.Login_InfoTitle = getValue(dict, "Login.InfoTitle") @@ -4978,11 +5148,14 @@ public final class PresentationStrings { self._Notification_PinnedLiveLocationMessage_r = extractArgumentRanges(self._Notification_PinnedLiveLocationMessage) self.AccessDenied_PhotosRestricted = getValue(dict, "AccessDenied.PhotosRestricted") self.Map_Locating = getValue(dict, "Map.Locating") + self.AutoDownloadSettings_Unlimited = getValue(dict, "AutoDownloadSettings.Unlimited") + self.MediaPicker_LivePhotoDescription = getValue(dict, "MediaPicker.LivePhotoDescription") 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.TermsOfService_Decline = getValue(dict, "TermsOfService.Decline") self.CallSettings_Always = getValue(dict, "CallSettings.Always") self.Message_ImageExpired = getValue(dict, "Message.ImageExpired") self.Channel_BanUser_Unban = getValue(dict, "Channel.BanUser.Unban") @@ -5004,11 +5177,13 @@ public final class PresentationStrings { self.Map_PullUpForPlaces = getValue(dict, "Map.PullUpForPlaces") self._Conversation_EncryptionWaiting = getValue(dict, "Conversation.EncryptionWaiting") self._Conversation_EncryptionWaiting_r = extractArgumentRanges(self._Conversation_EncryptionWaiting) + self.InfoPlist_NSSiriUsageDescription = getValue(dict, "InfoPlist.NSSiriUsageDescription") self.Calls_NotNow = getValue(dict, "Calls.NotNow") self.Conversation_Report = getValue(dict, "Conversation.Report") self._CHANNEL_MESSAGE_DOC = getValue(dict, "CHANNEL_MESSAGE_DOC") self._CHANNEL_MESSAGE_DOC_r = extractArgumentRanges(self._CHANNEL_MESSAGE_DOC) self.Channel_AdminLogFilter_EventsAll = getValue(dict, "Channel.AdminLogFilter.EventsAll") + self.InfoPlist_NSLocationWhenInUseUsageDescription = getValue(dict, "InfoPlist.NSLocationWhenInUseUsageDescription") self.Call_ConnectionErrorTitle = getValue(dict, "Call.ConnectionErrorTitle") self.Settings_ApplyProxyAlertEnable = getValue(dict, "Settings.ApplyProxyAlertEnable") self.Settings_ChatSettings = getValue(dict, "Settings.ChatSettings") @@ -5029,6 +5204,8 @@ public final class PresentationStrings { 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._AutoNightTheme_AutomaticHelp = getValue(dict, "AutoNightTheme.AutomaticHelp") + self._AutoNightTheme_AutomaticHelp_r = extractArgumentRanges(self._AutoNightTheme_AutomaticHelp) self.PhotoEditor_QualityTool = getValue(dict, "PhotoEditor.QualityTool") self.Login_NetworkError = getValue(dict, "Login.NetworkError") self.TwoStepAuth_EnterPasswordForgot = getValue(dict, "TwoStepAuth.EnterPasswordForgot") @@ -5064,6 +5241,7 @@ public final class PresentationStrings { self._MESSAGE_GEO = getValue(dict, "MESSAGE_GEO") self._MESSAGE_GEO_r = extractArgumentRanges(self._MESSAGE_GEO) self.Privacy_Calls = getValue(dict, "Privacy.Calls") + self.DialogList_AdLabel = getValue(dict, "DialogList.AdLabel") self.Channel_AdminLogFilter_EventsInfo = getValue(dict, "Channel.AdminLogFilter.EventsInfo") self._Channel_AdminLog_MessagePinned = getValue(dict, "Channel.AdminLog.MessagePinned") self._Channel_AdminLog_MessagePinned_r = extractArgumentRanges(self._Channel_AdminLog_MessagePinned) @@ -5076,8 +5254,10 @@ public final class PresentationStrings { self._Checkout_SavePasswordTimeoutAndTouchId = getValue(dict, "Checkout.SavePasswordTimeoutAndTouchId") self._Checkout_SavePasswordTimeoutAndTouchId_r = extractArgumentRanges(self._Checkout_SavePasswordTimeoutAndTouchId) self.HashtagSearch_AllChats = getValue(dict, "HashtagSearch.AllChats") + self.InfoPlist_NSPhotoLibraryAddUsageDescription = getValue(dict, "InfoPlist.NSPhotoLibraryAddUsageDescription") self._Date_ChatDateHeaderYear = getValue(dict, "Date.ChatDateHeaderYear") self._Date_ChatDateHeaderYear_r = extractArgumentRanges(self._Date_ChatDateHeaderYear) + self.Privacy_Calls_P2PContacts = getValue(dict, "Privacy.Calls.P2PContacts") self.CheckoutInfo_ShippingInfoCountry = getValue(dict, "CheckoutInfo.ShippingInfoCountry") self.Map_ShowPlaces = getValue(dict, "Map.ShowPlaces") self.Camera_VideoMode = getValue(dict, "Camera.VideoMode") @@ -5094,6 +5274,7 @@ public final class PresentationStrings { self.Privacy_PaymentsClearInfo = getValue(dict, "Privacy.PaymentsClearInfo") self.PhotoEditor_CurvesRed = getValue(dict, "PhotoEditor.CurvesRed") self.Privacy_PaymentsTitle = getValue(dict, "Privacy.PaymentsTitle") + self.SocksProxySetup_ProxyType = getValue(dict, "SocksProxySetup.ProxyType") 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") @@ -5132,6 +5313,7 @@ public final class PresentationStrings { self.AccessDenied_Camera = getValue(dict, "AccessDenied.Camera") self.WatchRemote_NotificationText = getValue(dict, "WatchRemote.NotificationText") self.SharedMedia_ViewInChat = getValue(dict, "SharedMedia.ViewInChat") + self.SecureId_FormRequestedTitle = getValue(dict, "SecureId.FormRequestedTitle") self.Activity_RecordingAudio = getValue(dict, "Activity.RecordingAudio") self.Watch_Stickers_StickerPacks = getValue(dict, "Watch.Stickers.StickerPacks") self._Target_ShareGameConfirmationPrivate = getValue(dict, "Target.ShareGameConfirmationPrivate") @@ -5148,6 +5330,7 @@ public final class PresentationStrings { self.MediaPicker_VideoMuteDescription = getValue(dict, "MediaPicker.VideoMuteDescription") self.UserInfo_ShareMyContactInfo = getValue(dict, "UserInfo.ShareMyContactInfo") self.Channel_Info_Stickers = getValue(dict, "Channel.Info.Stickers") + self.Appearance_ColorTheme = getValue(dict, "Appearance.ColorTheme") self._FileSize_GB = getValue(dict, "FileSize.GB") self._FileSize_GB_r = extractArgumentRanges(self._FileSize_GB) self.Month_ShortJanuary = getValue(dict, "Month.ShortJanuary") @@ -5168,10 +5351,12 @@ public final class PresentationStrings { self.ChangePhoneNumberCode_Help = getValue(dict, "ChangePhoneNumberCode.Help") self.Web_Error = getValue(dict, "Web.Error") self.ShareFileTip_Title = getValue(dict, "ShareFileTip.Title") + self.Privacy_SecretChatsLinkPreviews = getValue(dict, "Privacy.SecretChatsLinkPreviews") self.Username_InvalidStartsWithNumber = getValue(dict, "Username.InvalidStartsWithNumber") self._DialogList_EncryptedChatStartedIncoming = getValue(dict, "DialogList.EncryptedChatStartedIncoming") self._DialogList_EncryptedChatStartedIncoming_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedIncoming) self.Calls_AddTab = getValue(dict, "Calls.AddTab") + self.DialogList_AdNoticeAlert = getValue(dict, "DialogList.AdNoticeAlert") self.PhotoEditor_TiltShift = getValue(dict, "PhotoEditor.TiltShift") self.ChannelMembers_WhoCanAddMembers_Admins = getValue(dict, "ChannelMembers.WhoCanAddMembers.Admins") self.Tour_Text5 = getValue(dict, "Tour.Text5") @@ -5186,6 +5371,7 @@ public final class PresentationStrings { self.FastTwoStepSetup_EmailHelp = getValue(dict, "FastTwoStepSetup.EmailHelp") self.Month_GenOctober = getValue(dict, "Month.GenOctober") self.CheckoutInfo_ErrorPhoneInvalid = getValue(dict, "CheckoutInfo.ErrorPhoneInvalid") + self.AutoNightTheme_UpdateLocation = getValue(dict, "AutoNightTheme.UpdateLocation") self.Group_Setup_TypePublic = getValue(dict, "Group.Setup.TypePublic") self.Checkout_PaymentMethod_New = getValue(dict, "Checkout.PaymentMethod.New") self.ShareMenu_Comment = getValue(dict, "ShareMenu.Comment") @@ -5193,6 +5379,7 @@ public final class PresentationStrings { self.TwoStepAuth_SetPasswordHelp = getValue(dict, "TwoStepAuth.SetPasswordHelp") self.Channel_AdminLogFilter_EventsTitle = getValue(dict, "Channel.AdminLogFilter.EventsTitle") self.NotificationSettings_ContactJoined = getValue(dict, "NotificationSettings.ContactJoined") + self.ChatSettings_AutoDownloadVideos = getValue(dict, "ChatSettings.AutoDownloadVideos") 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) @@ -5204,6 +5391,7 @@ public final class PresentationStrings { self.Map_OpenInYandexMaps = getValue(dict, "Map.OpenInYandexMaps") self.FastTwoStepSetup_PasswordHelp = getValue(dict, "FastTwoStepSetup.PasswordHelp") self.GroupInfo_GroupHistoryHidden = getValue(dict, "GroupInfo.GroupHistoryHidden") + self.AutoNightTheme_UseSunsetSunrise = getValue(dict, "AutoNightTheme.UseSunsetSunrise") self.Month_ShortNovember = getValue(dict, "Month.ShortNovember") self.AccessDenied_Settings = getValue(dict, "AccessDenied.Settings") self.EncryptionKey_Title = getValue(dict, "EncryptionKey.Title") @@ -5219,6 +5407,7 @@ public final class PresentationStrings { self.Login_InfoFirstNamePlaceholder = getValue(dict, "Login.InfoFirstNamePlaceholder") self.Checkout_ErrorProviderAccountInvalid = getValue(dict, "Checkout.ErrorProviderAccountInvalid") self.CallSettings_TabIconDescription = getValue(dict, "CallSettings.TabIconDescription") + self.ChatSettings_AutoDownloadReset = getValue(dict, "ChatSettings.AutoDownloadReset") self.Checkout_WebConfirmation_Title = getValue(dict, "Checkout.WebConfirmation.Title") self.PasscodeSettings_AutoLock = getValue(dict, "PasscodeSettings.AutoLock") self.Notifications_MessageNotificationsPreview = getValue(dict, "Notifications.MessageNotificationsPreview") @@ -5238,9 +5427,11 @@ public final class PresentationStrings { self.DialogList_DeleteBotConfirmation = getValue(dict, "DialogList.DeleteBotConfirmation") self.EditProfile_Title = getValue(dict, "EditProfile.Title") self.PasscodeSettings_HelpTop = getValue(dict, "PasscodeSettings.HelpTop") + self.SocksProxySetup_ProxySocks5 = getValue(dict, "SocksProxySetup.ProxySocks5") self.Common_TakePhotoOrVideo = getValue(dict, "Common.TakePhotoOrVideo") self.Notification_MessageLifetime2s = getValue(dict, "Notification.MessageLifetime2s") self.Checkout_ErrorGeneric = getValue(dict, "Checkout.ErrorGeneric") + self.AutoNightTheme_Automatic = getValue(dict, "AutoNightTheme.Automatic") self.Channel_AdminLog_CanBanUsers = getValue(dict, "Channel.AdminLog.CanBanUsers") self.Cache_Indexing = getValue(dict, "Cache.Indexing") self._ENCRYPTION_REQUEST = getValue(dict, "ENCRYPTION_REQUEST") @@ -5258,8 +5449,10 @@ public final class PresentationStrings { self.KeyCommand_FocusOnInputField = getValue(dict, "KeyCommand.FocusOnInputField") self.Channel_Members_AddAdminErrorBlacklisted = getValue(dict, "Channel.Members.AddAdminErrorBlacklisted") self.Cache_KeepMedia = getValue(dict, "Cache.KeepMedia") + self.SocksProxySetup_ProxyTelegram = getValue(dict, "SocksProxySetup.ProxyTelegram") self.WebPreview_GettingLinkInfo = getValue(dict, "WebPreview.GettingLinkInfo") self.Group_Setup_TypePublicHelp = getValue(dict, "Group.Setup.TypePublicHelp") + self.Login_PRIVACY_URL = getValue(dict, "Login.PRIVACY_URL") self.Map_Satellite = getValue(dict, "Map.Satellite") self.Username_InvalidTaken = getValue(dict, "Username.InvalidTaken") self._Notification_PinnedAudioMessage = getValue(dict, "Notification.PinnedAudioMessage") @@ -5299,7 +5492,6 @@ public final class PresentationStrings { self.ChannelInfo_DeleteChannelConfirmation = getValue(dict, "ChannelInfo.DeleteChannelConfirmation") self.Weekday_ShortSaturday = getValue(dict, "Weekday.ShortSaturday") self.Map_SendThisLocation = getValue(dict, "Map.SendThisLocation") - self.ChatSettings_AutomaticMediaDownloadMaster = getValue(dict, "ChatSettings.AutomaticMediaDownloadMaster") self._Notification_PinnedDocumentMessage = getValue(dict, "Notification.PinnedDocumentMessage") self._Notification_PinnedDocumentMessage_r = extractArgumentRanges(self._Notification_PinnedDocumentMessage) self.Conversation_ContextMenuReply = getValue(dict, "Conversation.ContextMenuReply") @@ -5328,21 +5520,24 @@ public final class PresentationStrings { self._SecretImage_NotViewedYet_r = extractArgumentRanges(self._SecretImage_NotViewedYet) self.MaskStickerSettings_Title = getValue(dict, "MaskStickerSettings.Title") self.TwoStepAuth_SetPassword = getValue(dict, "TwoStepAuth.SetPassword") + self.SocksProxySetup_SavedProxies = getValue(dict, "SocksProxySetup.SavedProxies") self.GroupInfo_InviteLink_ShareLink = getValue(dict, "GroupInfo.InviteLink.ShareLink") - self.ChatSettings_AutomaticDownloadFile = getValue(dict, "ChatSettings.AutomaticDownloadFile") self.Common_Cancel = getValue(dict, "Common.Cancel") self.UserInfo_About_Placeholder = getValue(dict, "UserInfo.About.Placeholder") + self.Camera_Discard = getValue(dict, "Camera.Discard") 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.Privacy_SecretChatsTitle = getValue(dict, "Privacy.SecretChatsTitle") 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.AutoNightTheme_ScheduledFrom = getValue(dict, "AutoNightTheme.ScheduledFrom") self.Watch_Conversation_Reply = getValue(dict, "Watch.Conversation.Reply") self.ShareMenu_CopyShareLink = getValue(dict, "ShareMenu.CopyShareLink") self.Stickers_Search = getValue(dict, "Stickers.Search") @@ -5360,6 +5555,7 @@ public final class PresentationStrings { self._Notification_ChangedGroupName_r = extractArgumentRanges(self._Notification_ChangedGroupName) self._MESSAGE_VIDEO = getValue(dict, "MESSAGE_VIDEO") self._MESSAGE_VIDEO_r = extractArgumentRanges(self._MESSAGE_VIDEO) + self.TermsOfService_Title = getValue(dict, "TermsOfService.Title") self._Checkout_PayPrice = getValue(dict, "Checkout.PayPrice") self._Checkout_PayPrice_r = extractArgumentRanges(self._Checkout_PayPrice) self._Notification_PinnedTextMessage = getValue(dict, "Notification.PinnedTextMessage") @@ -5422,6 +5618,7 @@ public final class PresentationStrings { self.KeyCommand_ScrollUp = getValue(dict, "KeyCommand.ScrollUp") self._Privacy_GroupsAndChannels_InviteToChannelError = getValue(dict, "Privacy.GroupsAndChannels.InviteToChannelError") self._Privacy_GroupsAndChannels_InviteToChannelError_r = extractArgumentRanges(self._Privacy_GroupsAndChannels_InviteToChannelError) + self.AuthSessions_Sessions = getValue(dict, "AuthSessions.Sessions") self.Document_TargetConfirmationFormat = getValue(dict, "Document.TargetConfirmationFormat") self.Group_Setup_TypeHeader = getValue(dict, "Group.Setup.TypeHeader") self._DialogList_SinglePlayingGameSuffix = getValue(dict, "DialogList.SinglePlayingGameSuffix") @@ -5437,16 +5634,21 @@ public final class PresentationStrings { self.ReportPeer_ReasonPornography = getValue(dict, "ReportPeer.ReasonPornography") self.Notification_CreatedChannel = getValue(dict, "Notification.CreatedChannel") self.PhotoEditor_Original = getValue(dict, "PhotoEditor.Original") + self.TermsOfService_DeclineAndDelete = getValue(dict, "TermsOfService.DeclineAndDelete") self.Target_SelectGroup = getValue(dict, "Target.SelectGroup") + self.Stickers_SuggestAdded = getValue(dict, "Stickers.SuggestAdded") self.Channel_AdminLog_InfoPanelAlertTitle = getValue(dict, "Channel.AdminLog.InfoPanelAlertTitle") self.Notifications_GroupNotificationsPreview = getValue(dict, "Notifications.GroupNotificationsPreview") + self.ChatSettings_AutoDownloadPhotos = getValue(dict, "ChatSettings.AutoDownloadPhotos") self.SecureId_FormFieldEmail = getValue(dict, "SecureId.FormFieldEmail") self.Message_PinnedLocationMessage = getValue(dict, "Message.PinnedLocationMessage") + self.Appearance_PreviewReplyText = getValue(dict, "Appearance.PreviewReplyText") self.Settings_Logout = getValue(dict, "Settings.Logout") self._UserInfo_BlockConfirmation = getValue(dict, "UserInfo.BlockConfirmation") self._UserInfo_BlockConfirmation_r = extractArgumentRanges(self._UserInfo_BlockConfirmation) self.Profile_Username = getValue(dict, "Profile.Username") self.Group_Username_InvalidTooShort = getValue(dict, "Group.Username.InvalidTooShort") + self.Appearance_AutoNightTheme = getValue(dict, "Appearance.AutoNightTheme") self.AuthSessions_TerminateOtherSessions = getValue(dict, "AuthSessions.TerminateOtherSessions") self.PasscodeSettings_TryAgainIn1Minute = getValue(dict, "PasscodeSettings.TryAgainIn1Minute") self.Notifications_InAppNotifications = getValue(dict, "Notifications.InAppNotifications") @@ -5454,8 +5656,10 @@ public final class PresentationStrings { self.EnterPasscode_ChangeTitle = getValue(dict, "EnterPasscode.ChangeTitle") self.Call_Decline = getValue(dict, "Call.Decline") self.UserInfo_AddPhone = getValue(dict, "UserInfo.AddPhone") + self.AutoNightTheme_Title = getValue(dict, "AutoNightTheme.Title") self.Activity_PlayingGame = getValue(dict, "Activity.PlayingGame") self.CheckoutInfo_ShippingInfoStatePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoStatePlaceholder") + self.SaveIncomingPhotosSettings_From = getValue(dict, "SaveIncomingPhotosSettings.From") self.Notifications_MessageNotificationsSound = getValue(dict, "Notifications.MessageNotificationsSound") self.Call_StatusWaiting = getValue(dict, "Call.StatusWaiting") self.SecureId_FormFieldIdentityPlaceholder = getValue(dict, "SecureId.FormFieldIdentityPlaceholder") @@ -5468,6 +5672,7 @@ public final class PresentationStrings { 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.AutoDownloadSettings_PhotosTitle = getValue(dict, "AutoDownloadSettings.PhotosTitle") self.Map_OpenInHereMaps = getValue(dict, "Map.OpenInHereMaps") self.Stickers_FavoriteStickers = getValue(dict, "Stickers.FavoriteStickers") self.CheckoutInfo_Pay = getValue(dict, "CheckoutInfo.Pay") @@ -5478,6 +5683,7 @@ public final class PresentationStrings { 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.SocksProxySetup_ProxyStatusConnecting = getValue(dict, "SocksProxySetup.ProxyStatusConnecting") self.PasscodeSettings_ChangePasscode = getValue(dict, "PasscodeSettings.ChangePasscode") self.TwoStepAuth_RecoveryCodeInvalid = getValue(dict, "TwoStepAuth.RecoveryCodeInvalid") self._Message_PaymentSent = getValue(dict, "Message.PaymentSent") @@ -5488,7 +5694,10 @@ public final class PresentationStrings { self._Conversation_RestrictedMediaTimed_r = extractArgumentRanges(self._Conversation_RestrictedMediaTimed) self.Login_InfoDeletePhoto = getValue(dict, "Login.InfoDeletePhoto") self.TwoStepAuth_RecoveryCodeExpired = getValue(dict, "TwoStepAuth.RecoveryCodeExpired") + self.AutoDownloadSettings_Channels = getValue(dict, "AutoDownloadSettings.Channels") + self.AutoDownloadSettings_Contacts = getValue(dict, "AutoDownloadSettings.Contacts") self.TwoStepAuth_EmailTitle = getValue(dict, "TwoStepAuth.EmailTitle") + self.Channel_AdminLog_ChannelEmptyText = getValue(dict, "Channel.AdminLog.ChannelEmptyText") self.Privacy_GroupsAndChannels_NeverAllow = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow") self.Conversation_RestrictedStickers = getValue(dict, "Conversation.RestrictedStickers") self.Conversation_AddContact = getValue(dict, "Conversation.AddContact") @@ -5516,6 +5725,7 @@ public final class PresentationStrings { self.PhoneNumberHelp_Help = getValue(dict, "PhoneNumberHelp.Help") 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.Channel_Stickers_YourStickers = getValue(dict, "Channel.Stickers.YourStickers") self._CHAT_CREATED = getValue(dict, "CHAT_CREATED") @@ -5525,6 +5735,7 @@ public final class PresentationStrings { self._PrivacySettings_LastSeenContactsPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsPlus) self.ChangePhoneNumberNumber_NewNumber = getValue(dict, "ChangePhoneNumberNumber.NewNumber") self.Compose_NewChannel = getValue(dict, "Compose.NewChannel") + self.Login_TermsOfServiceAgree = getValue(dict, "Login.TermsOfServiceAgree") self.Channel_AdminLog_CanChangeInviteLink = getValue(dict, "Channel.AdminLog.CanChangeInviteLink") self._Call_CallInProgressMessage = getValue(dict, "Call.CallInProgressMessage") self._Call_CallInProgressMessage_r = extractArgumentRanges(self._Call_CallInProgressMessage) @@ -5534,6 +5745,7 @@ public final class PresentationStrings { self._CancelResetAccount_TextSMS = getValue(dict, "CancelResetAccount.TextSMS") self._CancelResetAccount_TextSMS_r = extractArgumentRanges(self._CancelResetAccount_TextSMS) self.Channel_EditAdmin_PermissionInviteUsers = getValue(dict, "Channel.EditAdmin.PermissionInviteUsers") + self.Privacy_Calls_P2PNever = getValue(dict, "Privacy.Calls.P2PNever") self.GroupInfo_DeleteAndExit = getValue(dict, "GroupInfo.DeleteAndExit") self.GroupInfo_InviteLink_CopyLink = getValue(dict, "GroupInfo.InviteLink.CopyLink") self.Login_ResetAccountProtected_Title = getValue(dict, "Login.ResetAccountProtected.Title") @@ -5549,6 +5761,9 @@ public final class PresentationStrings { self._Username_UsernameIsAvailable = getValue(dict, "Username.UsernameIsAvailable") self._Username_UsernameIsAvailable_r = extractArgumentRanges(self._Username_UsernameIsAvailable) self.KeyCommand_JumpToNextUnreadChat = getValue(dict, "KeyCommand.JumpToNextUnreadChat") + self.InfoPlist_NSContactsUsageDescription = getValue(dict, "InfoPlist.NSContactsUsageDescription") + self._SocksProxySetup_ProxyStatusPing = getValue(dict, "SocksProxySetup.ProxyStatusPing") + self._SocksProxySetup_ProxyStatusPing_r = extractArgumentRanges(self._SocksProxySetup_ProxyStatusPing) self._Date_ChatDateHeader = getValue(dict, "Date.ChatDateHeader") self._Date_ChatDateHeader_r = extractArgumentRanges(self._Date_ChatDateHeader) self.Conversation_EncryptedDescriptionTitle = getValue(dict, "Conversation.EncryptedDescriptionTitle") @@ -5576,11 +5791,13 @@ public final class PresentationStrings { self.Coub_TapForSound = getValue(dict, "Coub.TapForSound") self.Compose_NewEncryptedChat = getValue(dict, "Compose.NewEncryptedChat") self.PhotoEditor_CropReset = getValue(dict, "PhotoEditor.CropReset") + self.Privacy_Calls_P2PAlways = getValue(dict, "Privacy.Calls.P2PAlways") self.Login_InvalidLastNameError = getValue(dict, "Login.InvalidLastNameError") self.Channel_Members_AddMembers = getValue(dict, "Channel.Members.AddMembers") self.Tour_Title2 = getValue(dict, "Tour.Title2") self.Login_TermsOfServiceHeader = getValue(dict, "Login.TermsOfServiceHeader") self.Channel_AdminLog_BanSendGifs = getValue(dict, "Channel.AdminLog.BanSendGifs") + self.InfoPlist_NSMicrophoneUsageDescription = getValue(dict, "InfoPlist.NSMicrophoneUsageDescription") self.AuthSessions_OtherSessions = getValue(dict, "AuthSessions.OtherSessions") self.Watch_UserInfo_Title = getValue(dict, "Watch.UserInfo.Title") self.InstantPage_FeedbackButton = getValue(dict, "InstantPage.FeedbackButton") @@ -5601,6 +5818,7 @@ public final class PresentationStrings { self.Tour_Text6 = getValue(dict, "Tour.Text6") self.PhotoEditor_WarmthTool = getValue(dict, "PhotoEditor.WarmthTool") self.Common_TakePhoto = getValue(dict, "Common.TakePhoto") + self.SocksProxySetup_AdNoticeHelp = getValue(dict, "SocksProxySetup.AdNoticeHelp") self.UserInfo_CreateNewContact = getValue(dict, "UserInfo.CreateNewContact") self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") @@ -5626,6 +5844,7 @@ public final class PresentationStrings { self.PhoneLabel_Title = getValue(dict, "PhoneLabel.Title") self.PrivacySettings_Passcode = getValue(dict, "PrivacySettings.Passcode") self.Paint_ClearConfirm = getValue(dict, "Paint.ClearConfirm") + self.SocksProxySetup_Secret = getValue(dict, "SocksProxySetup.Secret") self._Checkout_SavePasswordTimeout = getValue(dict, "Checkout.SavePasswordTimeout") self._Checkout_SavePasswordTimeout_r = extractArgumentRanges(self._Checkout_SavePasswordTimeout) self.PhotoEditor_BlurToolOff = getValue(dict, "PhotoEditor.BlurToolOff") @@ -5644,7 +5863,7 @@ public final class PresentationStrings { self.Embed_PlayingInPIP = getValue(dict, "Embed.PlayingInPIP") self.Localization_EnglishLanguageName = getValue(dict, "Localization.EnglishLanguageName") self.Call_StatusIncoming = getValue(dict, "Call.StatusIncoming") - self.SecureId_FormFieldsHeader = getValue(dict, "SecureId.FormFieldsHeader") + self.Settings_Appearance = getValue(dict, "Settings.Appearance") self.Settings_PrivacySettings = getValue(dict, "Settings.PrivacySettings") self.Conversation_SilentBroadcastTooltipOn = getValue(dict, "Conversation.SilentBroadcastTooltipOn") self._SecretVideo_NotViewedYet = getValue(dict, "SecretVideo.NotViewedYet") @@ -5652,6 +5871,7 @@ public final class PresentationStrings { self._CHAT_MESSAGE_GEO = getValue(dict, "CHAT_MESSAGE_GEO") self._CHAT_MESSAGE_GEO_r = extractArgumentRanges(self._CHAT_MESSAGE_GEO) self.DialogList_SearchLabel = getValue(dict, "DialogList.SearchLabel") + self.InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription = getValue(dict, "InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription") self.Login_CodeSentInternal = getValue(dict, "Login.CodeSentInternal") self.Channel_AdminLog_BanSendMessages = getValue(dict, "Channel.AdminLog.BanSendMessages") self.Channel_MessagePhotoRemoved = getValue(dict, "Channel.MessagePhotoRemoved") @@ -5663,13 +5883,16 @@ public final class PresentationStrings { self.Compose_Create = getValue(dict, "Compose.Create") self._LOCKED_MESSAGE = getValue(dict, "LOCKED_MESSAGE") self._LOCKED_MESSAGE_r = extractArgumentRanges(self._LOCKED_MESSAGE) + self.Conversation_ClearPrivateHistory = getValue(dict, "Conversation.ClearPrivateHistory") 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.Conversation_ContextMenuReport = getValue(dict, "Conversation.ContextMenuReport") self._Call_GroupFormat = getValue(dict, "Call.GroupFormat") self._Call_GroupFormat_r = extractArgumentRanges(self._Call_GroupFormat) self.Forward_ChannelReadOnly = getValue(dict, "Forward.ChannelReadOnly") self.Privacy_GroupsAndChannels_NeverAllow_Title = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow.Title") + self.AutoDownloadSettings_Reset = getValue(dict, "AutoDownloadSettings.Reset") 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") @@ -5683,8 +5906,10 @@ public final class PresentationStrings { self._AuthSessions_AppUnofficial = getValue(dict, "AuthSessions.AppUnofficial") self._AuthSessions_AppUnofficial_r = extractArgumentRanges(self._AuthSessions_AppUnofficial) self.SecureId_FormFieldEmailPlaceholder = getValue(dict, "SecureId.FormFieldEmailPlaceholder") + self.AutoNightTheme_Disabled = getValue(dict, "AutoNightTheme.Disabled") self.Conversation_ContextMenuBan = getValue(dict, "Conversation.ContextMenuBan") self.Channel_EditAdmin_PermissionsHeader = getValue(dict, "Channel.EditAdmin.PermissionsHeader") + self.SocksProxySetup_PortPlaceholder = getValue(dict, "SocksProxySetup.PortPlaceholder") self._DialogList_SingleUploadingVideoSuffix = getValue(dict, "DialogList.SingleUploadingVideoSuffix") self._DialogList_SingleUploadingVideoSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingVideoSuffix) self.Group_UpgradeNoticeHeader = getValue(dict, "Group.UpgradeNoticeHeader") @@ -5702,6 +5927,7 @@ public final class PresentationStrings { self.Checkout_NewCard_PostcodeTitle = getValue(dict, "Checkout.NewCard.PostcodeTitle") self._Channel_AdminLog_MessageRestricted = getValue(dict, "Channel.AdminLog.MessageRestricted") self._Channel_AdminLog_MessageRestricted_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestricted) + self.SocksProxySetup_SecretPlaceholder = getValue(dict, "SocksProxySetup.SecretPlaceholder") self.Channel_EditAdmin_PermissinAddAdminOn = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOn") self.WebSearch_GIFs = getValue(dict, "WebSearch.GIFs") self.Conversation_SavedMessages = getValue(dict, "Conversation.SavedMessages") @@ -5715,6 +5941,8 @@ public final class PresentationStrings { self.Common_Edit = getValue(dict, "Common.Edit") self.Conversation_OpenFile = getValue(dict, "Conversation.OpenFile") self.Message_PinnedDocumentMessage = getValue(dict, "Message.PinnedDocumentMessage") + self.AuthSessions_LogOut = getValue(dict, "AuthSessions.LogOut") + self.AutoDownloadSettings_PrivateChats = getValue(dict, "AutoDownloadSettings.PrivateChats") self.Checkout_TotalPaidAmount = getValue(dict, "Checkout.TotalPaidAmount") self.Conversation_UnsupportedMedia = getValue(dict, "Conversation.UnsupportedMedia") self._Message_ForwardedMessage = getValue(dict, "Message.ForwardedMessage") @@ -5730,6 +5958,7 @@ public final class PresentationStrings { self.Profile_CreateEncryptedChatError = getValue(dict, "Profile.CreateEncryptedChatError") self.Map_LocationTitle = getValue(dict, "Map.LocationTitle") self.Call_RateCall = getValue(dict, "Call.RateCall") + self.SocksProxySetup_PasswordPlaceholder = getValue(dict, "SocksProxySetup.PasswordPlaceholder") self.Message_ReplyActionButtonShowReceipt = getValue(dict, "Message.ReplyActionButtonShowReceipt") self.PhotoEditor_ShadowsTool = getValue(dict, "PhotoEditor.ShadowsTool") self.Checkout_NewCard_CardholderNamePlaceholder = getValue(dict, "Checkout.NewCard.CardholderNamePlaceholder") @@ -5753,7 +5982,6 @@ public final class PresentationStrings { self.Profile_ShareContactButton = getValue(dict, "Profile.ShareContactButton") self.Group_ErrorSendRestrictedStickers = getValue(dict, "Group.ErrorSendRestrictedStickers") self.Bot_GroupStatusDoesNotReadHistory = getValue(dict, "Bot.GroupStatusDoesNotReadHistory") - self.ChatSettings_AutomaticDownloadVideo = getValue(dict, "ChatSettings.AutomaticDownloadVideo") self.Notification_Mute1h = getValue(dict, "Notification.Mute1h") self.Settings_TabTitle = getValue(dict, "Settings.TabTitle") self.NetworkUsageSettings_MediaAudioDataSection = getValue(dict, "NetworkUsageSettings.MediaAudioDataSection") @@ -5794,7 +6022,10 @@ public final class PresentationStrings { self.Call_StatusNoAnswer = getValue(dict, "Call.StatusNoAnswer") self._SecureId_FormPolicy = getValue(dict, "SecureId.FormPolicy") self._SecureId_FormPolicy_r = extractArgumentRanges(self._SecureId_FormPolicy) + self.Channel_AdminLogFilter_EventsLeavingSubscribers = getValue(dict, "Channel.AdminLogFilter.EventsLeavingSubscribers") + self.TermsOfService_AgeVerificationTitle = getValue(dict, "TermsOfService.AgeVerificationTitle") self.Conversation_MessageDialogDelete = getValue(dict, "Conversation.MessageDialogDelete") + self.Appearance_PreviewOutgoingText = getValue(dict, "Appearance.PreviewOutgoingText") self.Username_Placeholder = getValue(dict, "Username.Placeholder") self._Notification_PinnedDeletedMessage = getValue(dict, "Notification.PinnedDeletedMessage") self._Notification_PinnedDeletedMessage_r = extractArgumentRanges(self._Notification_PinnedDeletedMessage) @@ -5805,6 +6036,7 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_VIDEO = getValue(dict, "CHANNEL_MESSAGE_VIDEO") self._CHANNEL_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_VIDEO) self.EnterPasscode_TouchId = getValue(dict, "EnterPasscode.TouchId") + self.AuthSessions_LoggedInWithTelegram = getValue(dict, "AuthSessions.LoggedInWithTelegram") self.Checkout_ErrorInvoiceAlreadyPaid = getValue(dict, "Checkout.ErrorInvoiceAlreadyPaid") self.ChatAdmins_Title = getValue(dict, "ChatAdmins.Title") self.ChannelMembers_WhoCanAddMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers") @@ -5825,6 +6057,7 @@ public final class PresentationStrings { self.GroupInfo_InviteLink_RevokeLink = getValue(dict, "GroupInfo.InviteLink.RevokeLink") self.Checkout_PaymentMethod_Title = getValue(dict, "Checkout.PaymentMethod.Title") self.Conversation_Unmute = getValue(dict, "Conversation.Unmute") + self.AutoDownloadSettings_DocumentsTitle = getValue(dict, "AutoDownloadSettings.DocumentsTitle") self.Notifications_MessageNotifications = getValue(dict, "Notifications.MessageNotifications") self.ChannelMembers_WhoCanAddMembersAdminsHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAdminsHelp") self.DialogList_DeleteBotConversationConfirmation = getValue(dict, "DialogList.DeleteBotConversationConfirmation") @@ -5876,6 +6109,8 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_GIF = getValue(dict, "CHANNEL_MESSAGE_GIF") self._CHANNEL_MESSAGE_GIF_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GIF) self.Channel_AdminLogFilter_EventsEditedMessages = getValue(dict, "Channel.AdminLogFilter.EventsEditedMessages") + self.AutoNightTheme_ScheduleSection = getValue(dict, "AutoNightTheme.ScheduleSection") + self.Appearance_ThemeNightBlue = getValue(dict, "Appearance.ThemeNightBlue") 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") @@ -5892,6 +6127,7 @@ public final class PresentationStrings { self.Message_LiveLocation = getValue(dict, "Message.LiveLocation") self.NetworkUsageSettings_Title = getValue(dict, "NetworkUsageSettings.Title") self.CheckoutInfo_ShippingInfoPostcodePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoPostcodePlaceholder") + self.InfoPlist_NSPhotoLibraryUsageDescription = getValue(dict, "InfoPlist.NSPhotoLibraryUsageDescription") 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") @@ -5910,6 +6146,7 @@ public final class PresentationStrings { self.SearchImages_NoImagesFound = getValue(dict, "SearchImages.NoImagesFound") self._Watch_Time_ShortTodayAt = getValue(dict, "Watch.Time.ShortTodayAt") self._Watch_Time_ShortTodayAt_r = extractArgumentRanges(self._Watch_Time_ShortTodayAt) + self.Channel_AdminLogFilter_EventsNewSubscribers = getValue(dict, "Channel.AdminLogFilter.EventsNewSubscribers") self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") self.Message_PinnedContactMessage = getValue(dict, "Message.PinnedContactMessage") self.AccessDenied_CameraDisabled = getValue(dict, "AccessDenied.CameraDisabled") @@ -5936,9 +6173,13 @@ public final class PresentationStrings { self._CHAT_MESSAGE_PHOTO_r = extractArgumentRanges(self._CHAT_MESSAGE_PHOTO) self._UserInfo_UnblockConfirmation = getValue(dict, "UserInfo.UnblockConfirmation") self._UserInfo_UnblockConfirmation_r = extractArgumentRanges(self._UserInfo_UnblockConfirmation) + self.Appearance_PickAccentColor = getValue(dict, "Appearance.PickAccentColor") self.UserInfo_ShareBot = getValue(dict, "UserInfo.ShareBot") + self.Settings_ProxyConnected = getValue(dict, "Settings.ProxyConnected") + self.ChatSettings_AutoDownloadVoiceMessages = getValue(dict, "ChatSettings.AutoDownloadVoiceMessages") self.TwoStepAuth_EmailSkip = getValue(dict, "TwoStepAuth.EmailSkip") self.Conversation_JumpToDate = getValue(dict, "Conversation.JumpToDate") + self.AutoDownloadSettings_VideoMessagesTitle = getValue(dict, "AutoDownloadSettings.VideoMessagesTitle") self.CheckoutInfo_ReceiverInfoEmailPlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoEmailPlaceholder") self.Message_Photo = getValue(dict, "Message.Photo") self.Conversation_ReportSpam = getValue(dict, "Conversation.ReportSpam") @@ -5949,6 +6190,7 @@ public final class PresentationStrings { self.DialogList_SearchSectionGlobal = getValue(dict, "DialogList.SearchSectionGlobal") self.ChangePhoneNumberNumber_NumberPlaceholder = getValue(dict, "ChangePhoneNumberNumber.NumberPlaceholder") self.GroupInfo_AddUserLeftError = getValue(dict, "GroupInfo.AddUserLeftError") + self.Appearance_ThemeDay = getValue(dict, "Appearance.ThemeDay") self.GroupInfo_GroupType = getValue(dict, "GroupInfo.GroupType") self.Watch_Suggestion_OnMyWay = getValue(dict, "Watch.Suggestion.OnMyWay") self.Checkout_NewCard_PaymentCard = getValue(dict, "Checkout.NewCard.PaymentCard") @@ -5975,6 +6217,7 @@ public final class PresentationStrings { self.GroupInfo_GroupHistory = getValue(dict, "GroupInfo.GroupHistory") self.Conversation_ApplyLocalization = getValue(dict, "Conversation.ApplyLocalization") self.FastTwoStepSetup_Title = getValue(dict, "FastTwoStepSetup.Title") + self.SocksProxySetup_ProxyStatusUnavailable = getValue(dict, "SocksProxySetup.ProxyStatusUnavailable") self.Conversation_DeleteManyMessages = getValue(dict, "Conversation.DeleteManyMessages") self.CancelResetAccount_Title = getValue(dict, "CancelResetAccount.Title") self.Notification_CallOutgoingShort = getValue(dict, "Notification.CallOutgoingShort") @@ -5998,6 +6241,7 @@ public final class PresentationStrings { self.Preview_DeletePhoto = getValue(dict, "Preview.DeletePhoto") self.GroupInfo_ChannelListNamePlaceholder = getValue(dict, "GroupInfo.ChannelListNamePlaceholder") self.PasscodeSettings_TurnPasscodeOn = getValue(dict, "PasscodeSettings.TurnPasscodeOn") + self.AuthSessions_LogOutApplicationsHelp = getValue(dict, "AuthSessions.LogOutApplicationsHelp") self._Channel_AdminLog_MessageChangedGroupStickerPack = getValue(dict, "Channel.AdminLog.MessageChangedGroupStickerPack") self._Channel_AdminLog_MessageChangedGroupStickerPack_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupStickerPack) self.DialogList_Unpin = getValue(dict, "DialogList.Unpin") @@ -6012,7 +6256,9 @@ public final class PresentationStrings { self._Notification_NewAuthDetected_r = extractArgumentRanges(self._Notification_NewAuthDetected) self._Channel_AdminLog_MessageRemovedGroupStickerPack = getValue(dict, "Channel.AdminLog.MessageRemovedGroupStickerPack") self._Channel_AdminLog_MessageRemovedGroupStickerPack_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedGroupStickerPack) + self.TermsOfService_Agree = getValue(dict, "TermsOfService.Agree") self.AccessDenied_VideoMessageCamera = getValue(dict, "AccessDenied.VideoMessageCamera") + self.Privacy_ContactsSyncHelp = getValue(dict, "Privacy.ContactsSyncHelp") self.Conversation_Search = getValue(dict, "Conversation.Search") self._Channel_Management_PromotedBy = getValue(dict, "Channel.Management.PromotedBy") self._Channel_Management_PromotedBy_r = extractArgumentRanges(self._Channel_Management_PromotedBy) @@ -6027,6 +6273,7 @@ public final class PresentationStrings { 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.SocksProxySetup_UseProxy = getValue(dict, "SocksProxySetup.UseProxy") self.Group_UpgradeNoticeText1 = getValue(dict, "Group.UpgradeNoticeText1") self.ChatSettings_Other = getValue(dict, "ChatSettings.Other") self._Channel_AdminLog_MessageChangedChannelAbout = getValue(dict, "Channel.AdminLog.MessageChangedChannelAbout") @@ -6040,9 +6287,12 @@ public final class PresentationStrings { self.GroupInfo_InviteLink_Help = getValue(dict, "GroupInfo.InviteLink.Help") self.Calls_Missed = getValue(dict, "Calls.Missed") self.Conversation_ContextMenuForward = getValue(dict, "Conversation.ContextMenuForward") + self.AutoDownloadSettings_ResetHelp = getValue(dict, "AutoDownloadSettings.ResetHelp") self.Call_StatusRinging = getValue(dict, "Call.StatusRinging") self.Invitation_JoinGroup = getValue(dict, "Invitation.JoinGroup") self.Notification_PinnedMessage = getValue(dict, "Notification.PinnedMessage") + self.AutoDownloadSettings_WiFi = getValue(dict, "AutoDownloadSettings.WiFi") + self.Conversation_ClearSelfHistory = getValue(dict, "Conversation.ClearSelfHistory") self.Message_Location = getValue(dict, "Message.Location") self._Notification_MessageLifetimeChanged = getValue(dict, "Notification.MessageLifetimeChanged") self._Notification_MessageLifetimeChanged_r = extractArgumentRanges(self._Notification_MessageLifetimeChanged) @@ -6068,11 +6318,13 @@ public final class PresentationStrings { self._TwoStepAuth_EnterPasswordHint = getValue(dict, "TwoStepAuth.EnterPasswordHint") self._TwoStepAuth_EnterPasswordHint_r = extractArgumentRanges(self._TwoStepAuth_EnterPasswordHint) self.CallSettings_TabIcon = getValue(dict, "CallSettings.TabIcon") + self.TermsOfService_DeclineUnauthorized = getValue(dict, "TermsOfService.DeclineUnauthorized") self.ConversationProfile_UnknownAddMemberError = getValue(dict, "ConversationProfile.UnknownAddMemberError") self._Conversation_FileHowToText = getValue(dict, "Conversation.FileHowToText") self._Conversation_FileHowToText_r = extractArgumentRanges(self._Conversation_FileHowToText) self.Channel_AdminLog_BanSendMedia = getValue(dict, "Channel.AdminLog.BanSendMedia") self.Watch_UserInfo_Unblock = getValue(dict, "Watch.UserInfo.Unblock") + self.ChatSettings_AutoDownloadVideoMessages = getValue(dict, "ChatSettings.AutoDownloadVideoMessages") self.StickerPacksSettings_ArchivedMasks = getValue(dict, "StickerPacksSettings.ArchivedMasks") self.Message_Animation = getValue(dict, "Message.Animation") self.Checkout_PaymentMethod = getValue(dict, "Checkout.PaymentMethod") @@ -6081,6 +6333,8 @@ public final class PresentationStrings { self.Cache_Music = getValue(dict, "Cache.Music") self._Login_CallRequestState1 = getValue(dict, "Login.CallRequestState1") self._Login_CallRequestState1_r = extractArgumentRanges(self._Login_CallRequestState1) + self.Settings_ProxyDisabled = getValue(dict, "Settings.ProxyDisabled") + self.SocksProxySetup_Connecting = getValue(dict, "SocksProxySetup.Connecting") 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) @@ -6091,9 +6345,11 @@ public final class PresentationStrings { self.PhotoEditor_SaturationTool = getValue(dict, "PhotoEditor.SaturationTool") self.Channel_BanUser_BlockFor = getValue(dict, "Channel.BanUser.BlockFor") self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") + self.AutoNightTheme_NotAvailable = getValue(dict, "AutoNightTheme.NotAvailable") self.Bot_Start = getValue(dict, "Bot.Start") self._Channel_AdminLog_MessageChangedGroupAbout = getValue(dict, "Channel.AdminLog.MessageChangedGroupAbout") self._Channel_AdminLog_MessageChangedGroupAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupAbout) + self.Appearance_PreviewReplyAuthor = getValue(dict, "Appearance.PreviewReplyAuthor") self.Notifications_TextTone = getValue(dict, "Notifications.TextTone") self.Settings_CallSettings = getValue(dict, "Settings.CallSettings") self._Watch_Time_ShortYesterdayAt = getValue(dict, "Watch.Time.ShortYesterdayAt") @@ -6106,6 +6362,7 @@ public final class PresentationStrings { self.Channel_EditAdmin_PermissionDeleteMessages = getValue(dict, "Channel.EditAdmin.PermissionDeleteMessages") self.Channel_BanUser_PermissionSendStickersAndGifs = getValue(dict, "Channel.BanUser.PermissionSendStickersAndGifs") self.Conversation_CloudStorageInfo_Title = getValue(dict, "Conversation.CloudStorageInfo.Title") + self.Conversation_ClearSecretHistory = getValue(dict, "Conversation.ClearSecretHistory") self.Notification_RenamedChannel = getValue(dict, "Notification.RenamedChannel") self.BlockedUsers_BlockUser = getValue(dict, "BlockedUsers.BlockUser") self.ChatSettings_TextSize = getValue(dict, "ChatSettings.TextSize") @@ -6120,6 +6377,7 @@ public final class PresentationStrings { self.PhotoEditor_TintTool = getValue(dict, "PhotoEditor.TintTool") self.Watch_Suggestion_CantTalk = getValue(dict, "Watch.Suggestion.CantTalk") self.PhotoEditor_QualityHigh = getValue(dict, "PhotoEditor.QualityHigh") + self.SocksProxySetup_AddProxyTitle = getValue(dict, "SocksProxySetup.AddProxyTitle") self._CHAT_MESSAGE_STICKER = getValue(dict, "CHAT_MESSAGE_STICKER") self._CHAT_MESSAGE_STICKER_r = extractArgumentRanges(self._CHAT_MESSAGE_STICKER) self.Map_ChooseAPlace = getValue(dict, "Map.ChooseAPlace") @@ -6128,7 +6386,9 @@ 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.Privacy_ContactsSync = getValue(dict, "Privacy.ContactsSync") self.SocksProxySetup_Connection = getValue(dict, "SocksProxySetup.Connection") + self.SocksProxySetup_ProxyStatusChecking = getValue(dict, "SocksProxySetup.ProxyStatusChecking") self.Call_EncryptionKey_Title = getValue(dict, "Call.EncryptionKey.Title") self.PhotoEditor_BlurToolLinear = getValue(dict, "PhotoEditor.BlurToolLinear") self.AuthSessions_EmptyText = getValue(dict, "AuthSessions.EmptyText") @@ -6161,17 +6421,20 @@ public final class PresentationStrings { self.Map_YouAreHere = getValue(dict, "Map.YouAreHere") self.PhotoEditor_CurvesTool = getValue(dict, "PhotoEditor.CurvesTool") self.Map_LiveLocationFor1Hour = getValue(dict, "Map.LiveLocationFor1Hour") + self.AutoNightTheme_AutomaticSection = getValue(dict, "AutoNightTheme.AutomaticSection") self.Stickers_NoStickersFound = getValue(dict, "Stickers.NoStickersFound") self._Notification_JoinedChannel = getValue(dict, "Notification.JoinedChannel") self._Notification_JoinedChannel_r = extractArgumentRanges(self._Notification_JoinedChannel) self.GroupInfo_ActionRestrict = getValue(dict, "GroupInfo.ActionRestrict") self.Checkout_ShippingOption_Title = getValue(dict, "Checkout.ShippingOption.Title") + self.Stickers_SuggestStickers = getValue(dict, "Stickers.SuggestStickers") self._Channel_AdminLog_MessageKickedName = getValue(dict, "Channel.AdminLog.MessageKickedName") self._Channel_AdminLog_MessageKickedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageKickedName) self.Conversation_EncryptionProcessing = getValue(dict, "Conversation.EncryptionProcessing") 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.Privacy_ContactsResetConfirmation = getValue(dict, "Privacy.ContactsResetConfirmation") self.Month_ShortJune = getValue(dict, "Month.ShortJune") self.Privacy_Calls_Integration = getValue(dict, "Privacy.Calls.Integration") self.Channel_TypeSetup_Title = getValue(dict, "Channel.TypeSetup.Title") @@ -6227,6 +6490,7 @@ public final class PresentationStrings { self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") self.Conversation_FileDropbox = getValue(dict, "Conversation.FileDropbox") self.Login_PhonePlaceholder = getValue(dict, "Login.PhonePlaceholder") + self.SocksProxySetup_ProxyEnabled = getValue(dict, "SocksProxySetup.ProxyEnabled") self.Profile_MessageLifetime1d = getValue(dict, "Profile.MessageLifetime1d") self.CheckoutInfo_ShippingInfoCityPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCityPlaceholder") self.Calls_CallTabDescription = getValue(dict, "Calls.CallTabDescription") @@ -6239,6 +6503,7 @@ public final class PresentationStrings { self.Conversation_Info = getValue(dict, "Conversation.Info") self._Time_TodayAt = getValue(dict, "Time.TodayAt") self._Time_TodayAt_r = extractArgumentRanges(self._Time_TodayAt) + self.AutoDownloadSettings_VideosTitle = getValue(dict, "AutoDownloadSettings.VideosTitle") self.Conversation_Processing = getValue(dict, "Conversation.Processing") self.Conversation_RestrictedInline = getValue(dict, "Conversation.RestrictedInline") self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") @@ -6279,7 +6544,9 @@ public final class PresentationStrings { self.Tour_Text3 = getValue(dict, "Tour.Text3") self.Contacts_GlobalSearch = getValue(dict, "Contacts.GlobalSearch") self.DialogList_LanguageTooltip = getValue(dict, "DialogList.LanguageTooltip") + self.AuthSessions_LogOutApplications = getValue(dict, "AuthSessions.LogOutApplications") self.Map_LoadError = getValue(dict, "Map.LoadError") + self.Settings_ProxyConnecting = getValue(dict, "Settings.ProxyConnecting") self.AccessDenied_VoiceMicrophone = getValue(dict, "AccessDenied.VoiceMicrophone") self._CHANNEL_MESSAGE_STICKER = getValue(dict, "CHANNEL_MESSAGE_STICKER") self._CHANNEL_MESSAGE_STICKER_r = extractArgumentRanges(self._CHANNEL_MESSAGE_STICKER) @@ -6290,10 +6557,11 @@ public final class PresentationStrings { self.Channel_Status = getValue(dict, "Channel.Status") self.Map_ChooseLocationTitle = getValue(dict, "Map.ChooseLocationTitle") self.Map_OpenInYandexNavigator = getValue(dict, "Map.OpenInYandexNavigator") - self.ChatSettings_AutomaticDownloadPhoto = getValue(dict, "ChatSettings.AutomaticDownloadPhoto") + self.AutoNightTheme_PreferredTheme = getValue(dict, "AutoNightTheme.PreferredTheme") self.State_WaitingForNetwork = getValue(dict, "State.WaitingForNetwork") self.TwoStepAuth_EmailHelp = getValue(dict, "TwoStepAuth.EmailHelp") self.Conversation_StopLiveLocation = getValue(dict, "Conversation.StopLiveLocation") + self.Privacy_SecretChatsLinkPreviewsHelp = getValue(dict, "Privacy.SecretChatsLinkPreviewsHelp") self.PhotoEditor_SharpenTool = getValue(dict, "PhotoEditor.SharpenTool") self.Common_of = getValue(dict, "Common.of") self.AuthSessions_Title = getValue(dict, "AuthSessions.Title") @@ -6319,6 +6587,7 @@ public final class PresentationStrings { self._CHAT_ADD_YOU = getValue(dict, "CHAT_ADD_YOU") self._CHAT_ADD_YOU_r = extractArgumentRanges(self._CHAT_ADD_YOU) self.CheckoutInfo_ShippingInfoCity = getValue(dict, "CheckoutInfo.ShippingInfoCity") + self.AutoDownloadSettings_GroupChats = getValue(dict, "AutoDownloadSettings.GroupChats") self.Conversation_ClousStorageInfo_Description3 = getValue(dict, "Conversation.ClousStorageInfo.Description3") self.Conversation_PinMessageAlertGroup = getValue(dict, "Conversation.PinMessageAlertGroup") self.Settings_FAQ_Intro = getValue(dict, "Settings.FAQ_Intro") @@ -6337,6 +6606,7 @@ public final class PresentationStrings { self._Checkout_LiabilityAlert_r = extractArgumentRanges(self._Checkout_LiabilityAlert) self.Channel_Info_BlackList = getValue(dict, "Channel.Info.BlackList") self.Profile_BotInfo = getValue(dict, "Profile.BotInfo") + self.Stickers_SuggestAll = getValue(dict, "Stickers.SuggestAll") self.Compose_NewChannel_Members = getValue(dict, "Compose.NewChannel.Members") self.Notification_Reply = getValue(dict, "Notification.Reply") self.Watch_Stickers_Recents = getValue(dict, "Watch.Stickers.Recents") @@ -6346,15 +6616,21 @@ public final class PresentationStrings { self._MESSAGE_STICKER = getValue(dict, "MESSAGE_STICKER") self._MESSAGE_STICKER_r = extractArgumentRanges(self._MESSAGE_STICKER) self.Profile_MessageLifetime5s = getValue(dict, "Profile.MessageLifetime5s") + self.Privacy_ContactsReset = getValue(dict, "Privacy.ContactsReset") + self._TermsOfService_AgeVerificationText = getValue(dict, "TermsOfService.AgeVerificationText") + self._TermsOfService_AgeVerificationText_r = extractArgumentRanges(self._TermsOfService_AgeVerificationText) self._PINNED_PHOTO = getValue(dict, "PINNED_PHOTO") self._PINNED_PHOTO_r = extractArgumentRanges(self._PINNED_PHOTO) self.Channel_AdminLog_CanAddAdmins = getValue(dict, "Channel.AdminLog.CanAddAdmins") self.TwoStepAuth_SetupHint = getValue(dict, "TwoStepAuth.SetupHint") self.Conversation_StatusLeftGroup = getValue(dict, "Conversation.StatusLeftGroup") + self.ChatSettings_AutoDownloadDocuments = getValue(dict, "ChatSettings.AutoDownloadDocuments") self.MediaPicker_TapToUngroupDescription = getValue(dict, "MediaPicker.TapToUngroupDescription") self.Conversation_ShareBotLocationConfirmation = getValue(dict, "Conversation.ShareBotLocationConfirmation") self.Conversation_DeleteMessagesForMe = getValue(dict, "Conversation.DeleteMessagesForMe") self.Message_PinnedAnimationMessage = getValue(dict, "Message.PinnedAnimationMessage") + self.SocksProxySetup_ConnectAndSave = getValue(dict, "SocksProxySetup.ConnectAndSave") + self.SocksProxySetup_FailedToConnect = getValue(dict, "SocksProxySetup.FailedToConnect") self.Checkout_ErrorPrecheckoutFailed = getValue(dict, "Checkout.ErrorPrecheckoutFailed") self.Camera_PhotoMode = getValue(dict, "Camera.PhotoMode") self._Time_MonthOfYear_m2 = getValue(dict, "Time.MonthOfYear_m2") @@ -6372,6 +6648,7 @@ public final class PresentationStrings { self.Channel_ErrorAccessDenied = getValue(dict, "Channel.ErrorAccessDenied") self.Generic_ErrorMoreInfo = getValue(dict, "Generic.ErrorMoreInfo") self.Channel_AdminLog_TitleAllEvents = getValue(dict, "Channel.AdminLog.TitleAllEvents") + self.Settings_Proxy = getValue(dict, "Settings.Proxy") self.ChannelMembers_WhoCanAddMembersAllHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAllHelp") self.ChangePhoneNumberCode_CodePlaceholder = getValue(dict, "ChangePhoneNumberCode.CodePlaceholder") self.Camera_SquareMode = getValue(dict, "Camera.SquareMode") @@ -6381,6 +6658,7 @@ public final class PresentationStrings { self.Login_PadPhoneHelpTitle = getValue(dict, "Login.PadPhoneHelpTitle") self.Profile_CreateNewContact = getValue(dict, "Profile.CreateNewContact") self.AccessDenied_VideoMessageMicrophone = getValue(dict, "AccessDenied.VideoMessageMicrophone") + self.AutoDownloadSettings_VoiceMessagesTitle = getValue(dict, "AutoDownloadSettings.VoiceMessagesTitle") self.PhotoEditor_VignetteTool = getValue(dict, "PhotoEditor.VignetteTool") self.LastSeen_WithinAWeek = getValue(dict, "LastSeen.WithinAWeek") self.Widget_NoUsers = getValue(dict, "Widget.NoUsers") @@ -6389,6 +6667,7 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_AUDIO) self.DialogList_NoMessagesText = getValue(dict, "DialogList.NoMessagesText") self.MaskStickerSettings_Info = getValue(dict, "MaskStickerSettings.Info") + self.ChatSettings_AutoDownloadTitle = getValue(dict, "ChatSettings.AutoDownloadTitle") self.Conversation_FilePhotoOrVideo = getValue(dict, "Conversation.FilePhotoOrVideo") self.Channel_AdminLog_BanSendStickers = getValue(dict, "Channel.AdminLog.BanSendStickers") self.Common_Next = getValue(dict, "Common.Next") @@ -6398,6 +6677,7 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageRestrictedNewSetting_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNewSetting) self.GroupInfo_DeleteAndExitConfirmation = getValue(dict, "GroupInfo.DeleteAndExitConfirmation") self.TwoStepAuth_EmailInvalid = getValue(dict, "TwoStepAuth.EmailInvalid") + self.Privacy_ContactsTitle = getValue(dict, "Privacy.ContactsTitle") self._CHAT_MESSAGE_VIDEO = getValue(dict, "CHAT_MESSAGE_VIDEO") self._CHAT_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHAT_MESSAGE_VIDEO) self.Month_GenJune = getValue(dict, "Month.GenJune") @@ -6413,6 +6693,8 @@ public final class PresentationStrings { self.DialogList_RecentTitlePeople = getValue(dict, "DialogList.RecentTitlePeople") self.GroupInfo_Notifications = getValue(dict, "GroupInfo.Notifications") self.Call_ReportPlaceholder = getValue(dict, "Call.ReportPlaceholder") + self._AuthSessions_Message = getValue(dict, "AuthSessions.Message") + self._AuthSessions_Message_r = extractArgumentRanges(self._AuthSessions_Message) self._MESSAGE_DOC = getValue(dict, "MESSAGE_DOC") self._MESSAGE_DOC_r = extractArgumentRanges(self._MESSAGE_DOC) self.Group_Username_CreatePrivateLinkHelp = getValue(dict, "Group.Username.CreatePrivateLinkHelp") @@ -6452,7 +6734,6 @@ public final class PresentationStrings { self.Channel_AdminLog_EmptyFilterText = getValue(dict, "Channel.AdminLog.EmptyFilterText") self.Channel_AdminLog_EmptyText = getValue(dict, "Channel.AdminLog.EmptyText") self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") - self.Peer_DeletedUser = getValue(dict, "Peer.DeletedUser") self.PrivacyLastSeenSettings_CustomShareSettings_Delete = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettings.Delete") self._ENCRYPTED_MESSAGE = getValue(dict, "ENCRYPTED_MESSAGE") self._ENCRYPTED_MESSAGE_r = extractArgumentRanges(self._ENCRYPTED_MESSAGE) @@ -6480,6 +6761,7 @@ public final class PresentationStrings { self._Login_EmailPhoneSubject_r = extractArgumentRanges(self._Login_EmailPhoneSubject) self.Group_EditAdmin_PermissionChangeInfo = getValue(dict, "Group.EditAdmin.PermissionChangeInfo") self.TwoStepAuth_Email = getValue(dict, "TwoStepAuth.Email") + self.Stickers_SuggestNone = getValue(dict, "Stickers.SuggestNone") self.Map_SendMyCurrentLocation = getValue(dict, "Map.SendMyCurrentLocation") self._MESSAGE_ROUND = getValue(dict, "MESSAGE_ROUND") self._MESSAGE_ROUND_r = extractArgumentRanges(self._MESSAGE_ROUND) @@ -6488,6 +6770,7 @@ public final class PresentationStrings { self.AccessDenied_Title = getValue(dict, "AccessDenied.Title") self.SharedMedia_CategoryLinks = getValue(dict, "SharedMedia.CategoryLinks") self.Localization_LanguageOther = getValue(dict, "Localization.LanguageOther") + self.SaveIncomingPhotosSettings_Title = getValue(dict, "SaveIncomingPhotosSettings.Title") self.TwoStepAuth_EmailSkipAlert = getValue(dict, "TwoStepAuth.EmailSkipAlert") self.ChatSettings_Stickers = getValue(dict, "ChatSettings.Stickers") self.Camera_FlashOff = getValue(dict, "Camera.FlashOff") @@ -6508,7 +6791,6 @@ public final class PresentationStrings { self._PINNED_NOTEXT_r = extractArgumentRanges(self._PINNED_NOTEXT) self._Login_EmailCodeBody = getValue(dict, "Login.EmailCodeBody") self._Login_EmailCodeBody_r = extractArgumentRanges(self._Login_EmailCodeBody) - self.ChatSettings_AutomaticDownloadVideoMessage = getValue(dict, "ChatSettings.AutomaticDownloadVideoMessage") self.Profile_About = getValue(dict, "Profile.About") self._EncryptionKey_Description = getValue(dict, "EncryptionKey.Description") self._EncryptionKey_Description_r = extractArgumentRanges(self._EncryptionKey_Description) @@ -6525,9 +6807,11 @@ public final class PresentationStrings { self._Message_PinnedTextMessage_r = extractArgumentRanges(self._Message_PinnedTextMessage) self._Watch_Time_ShortWeekdayAt = getValue(dict, "Watch.Time.ShortWeekdayAt") self._Watch_Time_ShortWeekdayAt_r = extractArgumentRanges(self._Watch_Time_ShortWeekdayAt) + self.Conversation_EmptyGifPanelPlaceholder = getValue(dict, "Conversation.EmptyGifPanelPlaceholder") self.DialogList_Typing = getValue(dict, "DialogList.Typing") self.Notification_CallBack = getValue(dict, "Notification.CallBack") self.Map_LocatingError = getValue(dict, "Map.LocatingError") + self.InfoPlist_NSFaceIDUsageDescription = getValue(dict, "InfoPlist.NSFaceIDUsageDescription") self.MediaPicker_Send = getValue(dict, "MediaPicker.Send") self.ChannelIntro_Title = getValue(dict, "ChannelIntro.Title") self.AccessDenied_LocationAlwaysDenied = getValue(dict, "AccessDenied.LocationAlwaysDenied") @@ -6538,11 +6822,15 @@ public final class PresentationStrings { self.Channel_EditAdmin_CannotEdit = getValue(dict, "Channel.EditAdmin.CannotEdit") self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") self.BlockedUsers_Unblock = getValue(dict, "BlockedUsers.Unblock") + self.AutoDownloadSettings_Cellular = getValue(dict, "AutoDownloadSettings.Cellular") self._Time_MonthOfYear_m1 = getValue(dict, "Time.MonthOfYear_m1") self._Time_MonthOfYear_m1_r = extractArgumentRanges(self._Time_MonthOfYear_m1) + self.Appearance_PreviewIncomingText = getValue(dict, "Appearance.PreviewIncomingText") self.Notifications_GroupNotificationsAlert = getValue(dict, "Notifications.GroupNotificationsAlert") self.Paint_Masks = getValue(dict, "Paint.Masks") + self.Appearance_ThemeDayClassic = getValue(dict, "Appearance.ThemeDayClassic") self.StickerPack_ErrorNotFound = getValue(dict, "StickerPack.ErrorNotFound") + self.Appearance_ThemeNight = getValue(dict, "Appearance.ThemeNight") self.SecretTimer_ImageDescription = getValue(dict, "SecretTimer.ImageDescription") self._PINNED_CONTACT = getValue(dict, "PINNED_CONTACT") self._PINNED_CONTACT_r = extractArgumentRanges(self._PINNED_CONTACT) @@ -6553,6 +6841,7 @@ public final class PresentationStrings { self.Channel_AdminLog_EmptyTitle = getValue(dict, "Channel.AdminLog.EmptyTitle") self.PhotoEditor_Set = getValue(dict, "PhotoEditor.Set") self.LiveLocation_MenuStopAll = getValue(dict, "LiveLocation.MenuStopAll") + self.SocksProxySetup_AddProxy = getValue(dict, "SocksProxySetup.AddProxy") self._Notification_Invited = getValue(dict, "Notification.Invited") self._Notification_Invited_r = extractArgumentRanges(self._Notification_Invited) self.Watch_AuthRequired = getValue(dict, "Watch.AuthRequired") @@ -6560,6 +6849,7 @@ public final class PresentationStrings { self.AppleWatch_ReplyPresets = getValue(dict, "AppleWatch.ReplyPresets") self.Channel_Members_AddAdminErrorNotAMember = getValue(dict, "Channel.Members.AddAdminErrorNotAMember") self.Conversation_EncryptedDescription2 = getValue(dict, "Conversation.EncryptedDescription2") + self.SocksProxySetup_HostnamePlaceholder = getValue(dict, "SocksProxySetup.HostnamePlaceholder") self.NetworkUsageSettings_MediaVideoDataSection = getValue(dict, "NetworkUsageSettings.MediaVideoDataSection") self.Paint_Edit = getValue(dict, "Paint.Edit") self.Conversation_EncryptedDescription3 = getValue(dict, "Conversation.EncryptedDescription3") @@ -6585,6 +6875,8 @@ public final class PresentationStrings { self.ShareMenu_ShareTo = getValue(dict, "ShareMenu.ShareTo") self.Message_PinnedGame = getValue(dict, "Message.PinnedGame") self.Channel_AdminLog_CanSendMessages = getValue(dict, "Channel.AdminLog.CanSendMessages") + self._AutoNightTheme_LocationHelp = getValue(dict, "AutoNightTheme.LocationHelp") + self._AutoNightTheme_LocationHelp_r = extractArgumentRanges(self._AutoNightTheme_LocationHelp) self.Notification_RenamedGroup = getValue(dict, "Notification.RenamedGroup") self._Call_PrivacyErrorMessage = getValue(dict, "Call.PrivacyErrorMessage") self._Call_PrivacyErrorMessage_r = extractArgumentRanges(self._Call_PrivacyErrorMessage) @@ -6596,6 +6888,7 @@ public final class PresentationStrings { self.Preview_DeleteGif = getValue(dict, "Preview.DeleteGif") self.UserInfo_DeleteContact = getValue(dict, "UserInfo.DeleteContact") self.Notifications_ResetAllNotifications = getValue(dict, "Notifications.ResetAllNotifications") + self.SocksProxySetup_SaveProxy = getValue(dict, "SocksProxySetup.SaveProxy") self.Notification_MessageLifetimeRemovedOutgoing = getValue(dict, "Notification.MessageLifetimeRemovedOutgoing") self.Login_ContinueWithLocalization = getValue(dict, "Login.ContinueWithLocalization") self.GroupInfo_AddParticipant = getValue(dict, "GroupInfo.AddParticipant") @@ -6667,12 +6960,15 @@ public final class PresentationStrings { self._ChannelInfo_ChannelForbidden = getValue(dict, "ChannelInfo.ChannelForbidden") self._ChannelInfo_ChannelForbidden_r = extractArgumentRanges(self._ChannelInfo_ChannelForbidden) self.Conversation_ShareMyContactInfo = getValue(dict, "Conversation.ShareMyContactInfo") + self.SocksProxySetup_UsernamePlaceholder = getValue(dict, "SocksProxySetup.UsernamePlaceholder") 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.ChatSettings_AutoDownloadEnabled = getValue(dict, "ChatSettings.AutoDownloadEnabled") self.StickerPacksSettings_FeaturedPacks = getValue(dict, "StickerPacksSettings.FeaturedPacks") + self.AuthSessions_LoggedIn = getValue(dict, "AuthSessions.LoggedIn") self.Month_GenAugust = getValue(dict, "Month.GenAugust") self.Notification_CallCanceled = getValue(dict, "Notification.CallCanceled") self.Channel_Username_CreatePublicLinkHelp = getValue(dict, "Channel.Username.CreatePublicLinkHelp") @@ -6686,6 +6982,7 @@ public final class PresentationStrings { self.FastTwoStepSetup_PasswordConfirmationPlaceholder = getValue(dict, "FastTwoStepSetup.PasswordConfirmationPlaceholder") self.PasscodeSettings_Title = getValue(dict, "PasscodeSettings.Title") self.StickerPack_BuiltinPackName = getValue(dict, "StickerPack.BuiltinPackName") + self.Appearance_AccentColor = getValue(dict, "Appearance.AccentColor") self.Watch_Suggestion_BRB = getValue(dict, "Watch.Suggestion.BRB") self._CHAT_MESSAGE_ROUND = getValue(dict, "CHAT_MESSAGE_ROUND") self._CHAT_MESSAGE_ROUND_r = extractArgumentRanges(self._CHAT_MESSAGE_ROUND) @@ -6694,6 +6991,7 @@ public final class PresentationStrings { self.GroupInfo_LabelAdmin = getValue(dict, "GroupInfo.LabelAdmin") self.GroupInfo_Sound = getValue(dict, "GroupInfo.Sound") self.Channel_EditAdmin_PermissionBanUsers = getValue(dict, "Channel.EditAdmin.PermissionBanUsers") + self.InfoPlist_NSCameraUsageDescription = getValue(dict, "InfoPlist.NSCameraUsageDescription") self.Wallpaper_PhotoLibrary = getValue(dict, "Wallpaper.PhotoLibrary") self.Settings_About = getValue(dict, "Settings.About") self.Privacy_Calls_IntegrationHelp = getValue(dict, "Privacy.Calls.IntegrationHelp") @@ -6702,6 +7000,7 @@ public final class PresentationStrings { self.LoginPassword_ForgotPassword = getValue(dict, "LoginPassword.ForgotPassword") self._Map_LiveLocationShortHour = getValue(dict, "Map.LiveLocationShortHour") self._Map_LiveLocationShortHour_r = extractArgumentRanges(self._Map_LiveLocationShortHour) + self.Appearance_Preview = getValue(dict, "Appearance.Preview") self._DialogList_AwaitingEncryption = getValue(dict, "DialogList.AwaitingEncryption") self._DialogList_AwaitingEncryption_r = extractArgumentRanges(self._DialogList_AwaitingEncryption) self.ChatSettings_Appearance = getValue(dict, "ChatSettings.Appearance") @@ -6724,6 +7023,8 @@ public final class PresentationStrings { 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) + self._AutoDownloadSettings_UpTo = getValue(dict, "AutoDownloadSettings.UpTo") + self._AutoDownloadSettings_UpTo_r = extractArgumentRanges(self._AutoDownloadSettings_UpTo) self.Settings_ViewPhoto = getValue(dict, "Settings.ViewPhoto") self.Paint_RecentStickers = getValue(dict, "Paint.RecentStickers") self.Login_CallRequestState3 = getValue(dict, "Login.CallRequestState3") @@ -6742,12 +7043,12 @@ public final class PresentationStrings { self.Tour_Text4 = getValue(dict, "Tour.Text4") self.Channel_Info_Description = getValue(dict, "Channel.Info.Description") self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") + self.TermsOfService_Disagree = getValue(dict, "TermsOfService.Disagree") self.Watch_Compose_Send = getValue(dict, "Watch.Compose.Send") self.SocksProxySetup_UseForCallsHelp = getValue(dict, "SocksProxySetup.UseForCallsHelp") self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress") self.Settings_BlockedUsers = getValue(dict, "Settings.BlockedUsers") self.Month_ShortAugust = getValue(dict, "Month.ShortAugust") - self.ChatSettings_AutomaticMediaDownload = getValue(dict, "ChatSettings.AutomaticMediaDownload") self.Channel_AdminLogFilter_AdminsTitle = getValue(dict, "Channel.AdminLogFilter.AdminsTitle") self.Channel_EditAdmin_PermissionChangeInfo = getValue(dict, "Channel.EditAdmin.PermissionChangeInfo") self.Notifications_ResetAllNotificationsHelp = getValue(dict, "Notifications.ResetAllNotificationsHelp") @@ -6800,7 +7101,6 @@ public final class PresentationStrings { self.Group_ErrorAddBlocked = getValue(dict, "Group.ErrorAddBlocked") self.TwoStepAuth_AdditionalPassword = getValue(dict, "TwoStepAuth.AdditionalPassword") self.MediaPicker_Videos = getValue(dict, "MediaPicker.Videos") - self.ChatSettings_AutomaticDownloadReset = getValue(dict, "ChatSettings.AutomaticDownloadReset") self.BlockedUsers_AddNew = getValue(dict, "BlockedUsers.AddNew") self.StickerPacksSettings_StickerPacksSection = getValue(dict, "StickerPacksSettings.StickerPacksSection") self.Channel_NotificationLoading = getValue(dict, "Channel.NotificationLoading") @@ -6813,6 +7113,8 @@ public final class PresentationStrings { self.Checkout_EnterPassword = getValue(dict, "Checkout.EnterPassword") self.StickerPack_HideStickers = getValue(dict, "StickerPack.HideStickers") self.UserInfo_NotificationsEnabled = getValue(dict, "UserInfo.NotificationsEnabled") + self.InfoPlist_NSLocationAlwaysUsageDescription = getValue(dict, "InfoPlist.NSLocationAlwaysUsageDescription") + self.SocksProxySetup_ProxyDetailsTitle = getValue(dict, "SocksProxySetup.ProxyDetailsTitle") self.Weekday_ShortTuesday = getValue(dict, "Weekday.ShortTuesday") self.Notification_CallIncomingShort = getValue(dict, "Notification.CallIncomingShort") self.ConvertToSupergroup_Note = getValue(dict, "ConvertToSupergroup.Note") @@ -6821,494 +7123,496 @@ public final class PresentationStrings { self.StickerSettings_ContextHide = getValue(dict, "StickerSettings.ContextHide") self.Media_ShareThisPhoto = getValue(dict, "Media.ShareThisPhoto") self.Contacts_ShareTelegram = getValue(dict, "Contacts.ShareTelegram") + self.AutoNightTheme_Scheduled = getValue(dict, "AutoNightTheme.Scheduled") self.PrivacySettings_PasscodeAndFaceId = getValue(dict, "PrivacySettings.PasscodeAndFaceId") 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) - self._MessageTimer_Seconds_two = getValueWithForm(dict, "MessageTimer.Seconds", .two) - self._MessageTimer_Seconds_few = getValueWithForm(dict, "MessageTimer.Seconds", .few) - self._MessageTimer_Seconds_many = getValueWithForm(dict, "MessageTimer.Seconds", .many) - self._MessageTimer_Seconds_other = getValueWithForm(dict, "MessageTimer.Seconds", .other) - self._Call_Seconds_zero = getValueWithForm(dict, "Call.Seconds", .zero) - self._Call_Seconds_one = getValueWithForm(dict, "Call.Seconds", .one) - self._Call_Seconds_two = getValueWithForm(dict, "Call.Seconds", .two) - self._Call_Seconds_few = getValueWithForm(dict, "Call.Seconds", .few) - self._Call_Seconds_many = getValueWithForm(dict, "Call.Seconds", .many) - self._Call_Seconds_other = getValueWithForm(dict, "Call.Seconds", .other) - self._MessageTimer_ShortSeconds_zero = getValueWithForm(dict, "MessageTimer.ShortSeconds", .zero) - self._MessageTimer_ShortSeconds_one = getValueWithForm(dict, "MessageTimer.ShortSeconds", .one) - self._MessageTimer_ShortSeconds_two = getValueWithForm(dict, "MessageTimer.ShortSeconds", .two) - 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_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) - self._PasscodeSettings_FailedAttempts_few = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .few) - self._PasscodeSettings_FailedAttempts_many = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .many) - self._PasscodeSettings_FailedAttempts_other = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .other) - self._MuteFor_Hours_zero = getValueWithForm(dict, "MuteFor.Hours", .zero) - self._MuteFor_Hours_one = getValueWithForm(dict, "MuteFor.Hours", .one) - self._MuteFor_Hours_two = getValueWithForm(dict, "MuteFor.Hours", .two) - self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) - self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) - self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) - self._Media_ShareVideo_zero = getValueWithForm(dict, "Media.ShareVideo", .zero) - self._Media_ShareVideo_one = getValueWithForm(dict, "Media.ShareVideo", .one) - self._Media_ShareVideo_two = getValueWithForm(dict, "Media.ShareVideo", .two) - self._Media_ShareVideo_few = getValueWithForm(dict, "Media.ShareVideo", .few) - self._Media_ShareVideo_many = getValueWithForm(dict, "Media.ShareVideo", .many) - self._Media_ShareVideo_other = getValueWithForm(dict, "Media.ShareVideo", .other) - self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) - self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) - self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) - self._MessageTimer_ShortMinutes_few = getValueWithForm(dict, "MessageTimer.ShortMinutes", .few) - self._MessageTimer_ShortMinutes_many = getValueWithForm(dict, "MessageTimer.ShortMinutes", .many) - self._MessageTimer_ShortMinutes_other = getValueWithForm(dict, "MessageTimer.ShortMinutes", .other) - self._Notification_GameScoreSelfExtended_zero = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .zero) - self._Notification_GameScoreSelfExtended_one = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .one) - self._Notification_GameScoreSelfExtended_two = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .two) - self._Notification_GameScoreSelfExtended_few = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .few) - self._Notification_GameScoreSelfExtended_many = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .many) - self._Notification_GameScoreSelfExtended_other = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .other) - self._MessageTimer_ShortDays_zero = getValueWithForm(dict, "MessageTimer.ShortDays", .zero) - self._MessageTimer_ShortDays_one = getValueWithForm(dict, "MessageTimer.ShortDays", .one) - self._MessageTimer_ShortDays_two = getValueWithForm(dict, "MessageTimer.ShortDays", .two) - self._MessageTimer_ShortDays_few = getValueWithForm(dict, "MessageTimer.ShortDays", .few) - self._MessageTimer_ShortDays_many = getValueWithForm(dict, "MessageTimer.ShortDays", .many) - self._MessageTimer_ShortDays_other = getValueWithForm(dict, "MessageTimer.ShortDays", .other) - self._GroupInfo_ParticipantCount_zero = getValueWithForm(dict, "GroupInfo.ParticipantCount", .zero) - self._GroupInfo_ParticipantCount_one = getValueWithForm(dict, "GroupInfo.ParticipantCount", .one) - self._GroupInfo_ParticipantCount_two = getValueWithForm(dict, "GroupInfo.ParticipantCount", .two) - self._GroupInfo_ParticipantCount_few = getValueWithForm(dict, "GroupInfo.ParticipantCount", .few) - self._GroupInfo_ParticipantCount_many = getValueWithForm(dict, "GroupInfo.ParticipantCount", .many) - self._GroupInfo_ParticipantCount_other = getValueWithForm(dict, "GroupInfo.ParticipantCount", .other) - self._ForwardedPhotos_zero = getValueWithForm(dict, "ForwardedPhotos", .zero) - self._ForwardedPhotos_one = getValueWithForm(dict, "ForwardedPhotos", .one) - self._ForwardedPhotos_two = getValueWithForm(dict, "ForwardedPhotos", .two) - self._ForwardedPhotos_few = getValueWithForm(dict, "ForwardedPhotos", .few) - self._ForwardedPhotos_many = getValueWithForm(dict, "ForwardedPhotos", .many) - self._ForwardedPhotos_other = getValueWithForm(dict, "ForwardedPhotos", .other) - self._ServiceMessage_GameScoreSelfExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .zero) - self._ServiceMessage_GameScoreSelfExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .one) - self._ServiceMessage_GameScoreSelfExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .two) - self._ServiceMessage_GameScoreSelfExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .few) - self._ServiceMessage_GameScoreSelfExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .many) - self._ServiceMessage_GameScoreSelfExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .other) - self._Call_ShortSeconds_zero = getValueWithForm(dict, "Call.ShortSeconds", .zero) - self._Call_ShortSeconds_one = getValueWithForm(dict, "Call.ShortSeconds", .one) - self._Call_ShortSeconds_two = getValueWithForm(dict, "Call.ShortSeconds", .two) - 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._Conversation_StatusSubscribers_zero = getValueWithForm(dict, "Conversation.StatusSubscribers", .zero) - self._Conversation_StatusSubscribers_one = getValueWithForm(dict, "Conversation.StatusSubscribers", .one) - self._Conversation_StatusSubscribers_two = getValueWithForm(dict, "Conversation.StatusSubscribers", .two) - self._Conversation_StatusSubscribers_few = getValueWithForm(dict, "Conversation.StatusSubscribers", .few) - self._Conversation_StatusSubscribers_many = getValueWithForm(dict, "Conversation.StatusSubscribers", .many) - self._Conversation_StatusSubscribers_other = getValueWithForm(dict, "Conversation.StatusSubscribers", .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) - self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) - self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) - self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) - self._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) - self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) - self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) - self._ForwardedAudios_few = getValueWithForm(dict, "ForwardedAudios", .few) - self._ForwardedAudios_many = getValueWithForm(dict, "ForwardedAudios", .many) - self._ForwardedAudios_other = getValueWithForm(dict, "ForwardedAudios", .other) + self.TermsOfService_Confirm = getValue(dict, "TermsOfService.Confirm") self._PrivacyLastSeenSettings_AddUsers_zero = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .zero) self._PrivacyLastSeenSettings_AddUsers_one = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .one) self._PrivacyLastSeenSettings_AddUsers_two = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .two) self._PrivacyLastSeenSettings_AddUsers_few = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .few) self._PrivacyLastSeenSettings_AddUsers_many = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .many) self._PrivacyLastSeenSettings_AddUsers_other = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .other) - self._ForwardedVideoMessages_zero = getValueWithForm(dict, "ForwardedVideoMessages", .zero) - self._ForwardedVideoMessages_one = getValueWithForm(dict, "ForwardedVideoMessages", .one) - self._ForwardedVideoMessages_two = getValueWithForm(dict, "ForwardedVideoMessages", .two) - self._ForwardedVideoMessages_few = getValueWithForm(dict, "ForwardedVideoMessages", .few) - self._ForwardedVideoMessages_many = getValueWithForm(dict, "ForwardedVideoMessages", .many) - self._ForwardedVideoMessages_other = getValueWithForm(dict, "ForwardedVideoMessages", .other) - self._SharedMedia_Generic_zero = getValueWithForm(dict, "SharedMedia.Generic", .zero) - self._SharedMedia_Generic_one = getValueWithForm(dict, "SharedMedia.Generic", .one) - self._SharedMedia_Generic_two = getValueWithForm(dict, "SharedMedia.Generic", .two) - self._SharedMedia_Generic_few = getValueWithForm(dict, "SharedMedia.Generic", .few) - self._SharedMedia_Generic_many = getValueWithForm(dict, "SharedMedia.Generic", .many) - self._SharedMedia_Generic_other = getValueWithForm(dict, "SharedMedia.Generic", .other) - self._InviteText_ContactsCount_zero = getValueWithForm(dict, "InviteText.ContactsCount", .zero) - self._InviteText_ContactsCount_one = getValueWithForm(dict, "InviteText.ContactsCount", .one) - self._InviteText_ContactsCount_two = getValueWithForm(dict, "InviteText.ContactsCount", .two) - self._InviteText_ContactsCount_few = getValueWithForm(dict, "InviteText.ContactsCount", .few) - self._InviteText_ContactsCount_many = getValueWithForm(dict, "InviteText.ContactsCount", .many) - self._InviteText_ContactsCount_other = getValueWithForm(dict, "InviteText.ContactsCount", .other) - self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) - self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) - self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) - self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) - self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) - self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) - self._Conversation_LiveLocationMembersCount_zero = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .zero) - self._Conversation_LiveLocationMembersCount_one = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .one) - self._Conversation_LiveLocationMembersCount_two = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .two) - self._Conversation_LiveLocationMembersCount_few = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .few) - self._Conversation_LiveLocationMembersCount_many = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .many) - self._Conversation_LiveLocationMembersCount_other = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .other) - self._Media_SharePhoto_zero = getValueWithForm(dict, "Media.SharePhoto", .zero) - self._Media_SharePhoto_one = getValueWithForm(dict, "Media.SharePhoto", .one) - self._Media_SharePhoto_two = getValueWithForm(dict, "Media.SharePhoto", .two) - self._Media_SharePhoto_few = getValueWithForm(dict, "Media.SharePhoto", .few) - self._Media_SharePhoto_many = getValueWithForm(dict, "Media.SharePhoto", .many) - self._Media_SharePhoto_other = getValueWithForm(dict, "Media.SharePhoto", .other) - self._LiveLocation_MenuChatsCount_zero = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .zero) - self._LiveLocation_MenuChatsCount_one = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .one) - self._LiveLocation_MenuChatsCount_two = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .two) - self._LiveLocation_MenuChatsCount_few = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .few) - self._LiveLocation_MenuChatsCount_many = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .many) - self._LiveLocation_MenuChatsCount_other = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .other) - self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) - self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) - self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) - self._Invitation_Members_few = getValueWithForm(dict, "Invitation.Members", .few) - self._Invitation_Members_many = getValueWithForm(dict, "Invitation.Members", .many) - self._Invitation_Members_other = getValueWithForm(dict, "Invitation.Members", .other) - self._ForwardedFiles_zero = getValueWithForm(dict, "ForwardedFiles", .zero) - self._ForwardedFiles_one = getValueWithForm(dict, "ForwardedFiles", .one) - self._ForwardedFiles_two = getValueWithForm(dict, "ForwardedFiles", .two) - self._ForwardedFiles_few = getValueWithForm(dict, "ForwardedFiles", .few) - self._ForwardedFiles_many = getValueWithForm(dict, "ForwardedFiles", .many) - self._ForwardedFiles_other = getValueWithForm(dict, "ForwardedFiles", .other) - self._ForwardedStickers_zero = getValueWithForm(dict, "ForwardedStickers", .zero) - self._ForwardedStickers_one = getValueWithForm(dict, "ForwardedStickers", .one) - self._ForwardedStickers_two = getValueWithForm(dict, "ForwardedStickers", .two) - self._ForwardedStickers_few = getValueWithForm(dict, "ForwardedStickers", .few) - self._ForwardedStickers_many = getValueWithForm(dict, "ForwardedStickers", .many) - self._ForwardedStickers_other = getValueWithForm(dict, "ForwardedStickers", .other) - self._StickerPack_StickerCount_zero = getValueWithForm(dict, "StickerPack.StickerCount", .zero) - self._StickerPack_StickerCount_one = getValueWithForm(dict, "StickerPack.StickerCount", .one) - self._StickerPack_StickerCount_two = getValueWithForm(dict, "StickerPack.StickerCount", .two) - 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._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._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) - self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) - self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) - self._AttachmentMenu_SendVideo_few = getValueWithForm(dict, "AttachmentMenu.SendVideo", .few) - self._AttachmentMenu_SendVideo_many = getValueWithForm(dict, "AttachmentMenu.SendVideo", .many) - self._AttachmentMenu_SendVideo_other = getValueWithForm(dict, "AttachmentMenu.SendVideo", .other) - self._Call_Minutes_zero = getValueWithForm(dict, "Call.Minutes", .zero) - self._Call_Minutes_one = getValueWithForm(dict, "Call.Minutes", .one) - self._Call_Minutes_two = getValueWithForm(dict, "Call.Minutes", .two) - self._Call_Minutes_few = getValueWithForm(dict, "Call.Minutes", .few) - self._Call_Minutes_many = getValueWithForm(dict, "Call.Minutes", .many) - self._Call_Minutes_other = getValueWithForm(dict, "Call.Minutes", .other) - self._ForwardedContacts_zero = getValueWithForm(dict, "ForwardedContacts", .zero) - self._ForwardedContacts_one = getValueWithForm(dict, "ForwardedContacts", .one) - self._ForwardedContacts_two = getValueWithForm(dict, "ForwardedContacts", .two) - self._ForwardedContacts_few = getValueWithForm(dict, "ForwardedContacts", .few) - self._ForwardedContacts_many = getValueWithForm(dict, "ForwardedContacts", .many) - self._ForwardedContacts_other = getValueWithForm(dict, "ForwardedContacts", .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) - self._MessageTimer_ShortHours_few = getValueWithForm(dict, "MessageTimer.ShortHours", .few) - self._MessageTimer_ShortHours_many = getValueWithForm(dict, "MessageTimer.ShortHours", .many) - self._MessageTimer_ShortHours_other = getValueWithForm(dict, "MessageTimer.ShortHours", .other) - self._ServiceMessage_GameScoreExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .zero) - self._ServiceMessage_GameScoreExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .one) - self._ServiceMessage_GameScoreExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .two) - self._ServiceMessage_GameScoreExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .few) - self._ServiceMessage_GameScoreExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .many) - self._ServiceMessage_GameScoreExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .other) - self._StickerPack_AddStickerCount_zero = getValueWithForm(dict, "StickerPack.AddStickerCount", .zero) - self._StickerPack_AddStickerCount_one = getValueWithForm(dict, "StickerPack.AddStickerCount", .one) - self._StickerPack_AddStickerCount_two = getValueWithForm(dict, "StickerPack.AddStickerCount", .two) - self._StickerPack_AddStickerCount_few = getValueWithForm(dict, "StickerPack.AddStickerCount", .few) - self._StickerPack_AddStickerCount_many = getValueWithForm(dict, "StickerPack.AddStickerCount", .many) - self._StickerPack_AddStickerCount_other = getValueWithForm(dict, "StickerPack.AddStickerCount", .other) - self._AttachmentMenu_SendPhoto_zero = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .zero) - self._AttachmentMenu_SendPhoto_one = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .one) - self._AttachmentMenu_SendPhoto_two = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .two) - 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._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) - self._ServiceMessage_GameScoreSelfSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .few) - self._ServiceMessage_GameScoreSelfSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .many) - self._ServiceMessage_GameScoreSelfSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .other) - self._SharedMedia_Photo_zero = getValueWithForm(dict, "SharedMedia.Photo", .zero) - self._SharedMedia_Photo_one = getValueWithForm(dict, "SharedMedia.Photo", .one) - self._SharedMedia_Photo_two = getValueWithForm(dict, "SharedMedia.Photo", .two) - self._SharedMedia_Photo_few = getValueWithForm(dict, "SharedMedia.Photo", .few) - self._SharedMedia_Photo_many = getValueWithForm(dict, "SharedMedia.Photo", .many) - self._SharedMedia_Photo_other = getValueWithForm(dict, "SharedMedia.Photo", .other) - self._MessageTimer_Weeks_zero = getValueWithForm(dict, "MessageTimer.Weeks", .zero) - self._MessageTimer_Weeks_one = getValueWithForm(dict, "MessageTimer.Weeks", .one) - self._MessageTimer_Weeks_two = getValueWithForm(dict, "MessageTimer.Weeks", .two) - self._MessageTimer_Weeks_few = getValueWithForm(dict, "MessageTimer.Weeks", .few) - self._MessageTimer_Weeks_many = getValueWithForm(dict, "MessageTimer.Weeks", .many) - self._MessageTimer_Weeks_other = getValueWithForm(dict, "MessageTimer.Weeks", .other) - self._StickerPack_AddMaskCount_zero = getValueWithForm(dict, "StickerPack.AddMaskCount", .zero) - self._StickerPack_AddMaskCount_one = getValueWithForm(dict, "StickerPack.AddMaskCount", .one) - self._StickerPack_AddMaskCount_two = getValueWithForm(dict, "StickerPack.AddMaskCount", .two) - 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._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) - self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) - self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) - self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) - self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) - self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .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) - self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) - self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) - self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .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._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) - self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) - self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) - self._MuteExpires_Hours_few = getValueWithForm(dict, "MuteExpires.Hours", .few) - self._MuteExpires_Hours_many = getValueWithForm(dict, "MuteExpires.Hours", .many) - self._MuteExpires_Hours_other = getValueWithForm(dict, "MuteExpires.Hours", .other) - self._Watch_LastSeen_HoursAgo_zero = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .zero) - self._Watch_LastSeen_HoursAgo_one = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .one) - self._Watch_LastSeen_HoursAgo_two = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .two) - self._Watch_LastSeen_HoursAgo_few = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .few) - self._Watch_LastSeen_HoursAgo_many = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .many) - self._Watch_LastSeen_HoursAgo_other = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .other) - self._Forward_ConfirmMultipleFiles_zero = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .zero) - self._Forward_ConfirmMultipleFiles_one = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .one) - self._Forward_ConfirmMultipleFiles_two = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .two) - self._Forward_ConfirmMultipleFiles_few = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .few) - self._Forward_ConfirmMultipleFiles_many = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .many) - self._Forward_ConfirmMultipleFiles_other = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .other) - self._AttachmentMenu_SendGif_zero = getValueWithForm(dict, "AttachmentMenu.SendGif", .zero) - self._AttachmentMenu_SendGif_one = getValueWithForm(dict, "AttachmentMenu.SendGif", .one) - self._AttachmentMenu_SendGif_two = getValueWithForm(dict, "AttachmentMenu.SendGif", .two) - self._AttachmentMenu_SendGif_few = getValueWithForm(dict, "AttachmentMenu.SendGif", .few) - self._AttachmentMenu_SendGif_many = getValueWithForm(dict, "AttachmentMenu.SendGif", .many) - self._AttachmentMenu_SendGif_other = getValueWithForm(dict, "AttachmentMenu.SendGif", .other) - self._StickerPack_RemoveStickerCount_zero = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .zero) - self._StickerPack_RemoveStickerCount_one = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .one) - self._StickerPack_RemoveStickerCount_two = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .two) - self._StickerPack_RemoveStickerCount_few = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .few) - self._StickerPack_RemoveStickerCount_many = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .many) - self._StickerPack_RemoveStickerCount_other = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .other) - self._SharedMedia_Link_zero = getValueWithForm(dict, "SharedMedia.Link", .zero) - self._SharedMedia_Link_one = getValueWithForm(dict, "SharedMedia.Link", .one) - self._SharedMedia_Link_two = getValueWithForm(dict, "SharedMedia.Link", .two) - self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) - self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) - self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .other) - self._DialogList_LiveLocationChatsCount_zero = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .zero) - self._DialogList_LiveLocationChatsCount_one = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .one) - self._DialogList_LiveLocationChatsCount_two = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .two) - self._DialogList_LiveLocationChatsCount_few = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .few) - self._DialogList_LiveLocationChatsCount_many = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .many) - self._DialogList_LiveLocationChatsCount_other = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .other) - self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) - self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) - self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) - 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._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) - self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) - self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) - self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .other) - self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) - self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) - self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) - self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) - self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) - self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .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) - self._MessageTimer_Years_few = getValueWithForm(dict, "MessageTimer.Years", .few) - self._MessageTimer_Years_many = getValueWithForm(dict, "MessageTimer.Years", .many) - self._MessageTimer_Years_other = getValueWithForm(dict, "MessageTimer.Years", .other) - self._Map_ETAMinutes_zero = getValueWithForm(dict, "Map.ETAMinutes", .zero) - self._Map_ETAMinutes_one = getValueWithForm(dict, "Map.ETAMinutes", .one) - self._Map_ETAMinutes_two = getValueWithForm(dict, "Map.ETAMinutes", .two) - 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._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) - self._Notification_GameScoreSelfSimple_few = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .few) - self._Notification_GameScoreSelfSimple_many = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .many) - self._Notification_GameScoreSelfSimple_other = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .other) - self._ServiceMessage_GameScoreSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .zero) - self._ServiceMessage_GameScoreSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .one) - self._ServiceMessage_GameScoreSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .two) - self._ServiceMessage_GameScoreSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .few) - self._ServiceMessage_GameScoreSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .many) - self._ServiceMessage_GameScoreSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .other) - self._QuickSend_Photos_zero = getValueWithForm(dict, "QuickSend.Photos", .zero) - self._QuickSend_Photos_one = getValueWithForm(dict, "QuickSend.Photos", .one) - self._QuickSend_Photos_two = getValueWithForm(dict, "QuickSend.Photos", .two) - self._QuickSend_Photos_few = getValueWithForm(dict, "QuickSend.Photos", .few) - self._QuickSend_Photos_many = getValueWithForm(dict, "QuickSend.Photos", .many) - self._QuickSend_Photos_other = getValueWithForm(dict, "QuickSend.Photos", .other) self._MuteFor_Days_zero = getValueWithForm(dict, "MuteFor.Days", .zero) self._MuteFor_Days_one = getValueWithForm(dict, "MuteFor.Days", .one) self._MuteFor_Days_two = getValueWithForm(dict, "MuteFor.Days", .two) self._MuteFor_Days_few = getValueWithForm(dict, "MuteFor.Days", .few) self._MuteFor_Days_many = getValueWithForm(dict, "MuteFor.Days", .many) self._MuteFor_Days_other = getValueWithForm(dict, "MuteFor.Days", .other) - self._Conversation_StatusOnline_zero = getValueWithForm(dict, "Conversation.StatusOnline", .zero) - self._Conversation_StatusOnline_one = getValueWithForm(dict, "Conversation.StatusOnline", .one) - self._Conversation_StatusOnline_two = getValueWithForm(dict, "Conversation.StatusOnline", .two) - self._Conversation_StatusOnline_few = getValueWithForm(dict, "Conversation.StatusOnline", .few) - self._Conversation_StatusOnline_many = getValueWithForm(dict, "Conversation.StatusOnline", .many) - self._Conversation_StatusOnline_other = getValueWithForm(dict, "Conversation.StatusOnline", .other) - self._AttachmentMenu_SendItem_zero = getValueWithForm(dict, "AttachmentMenu.SendItem", .zero) - self._AttachmentMenu_SendItem_one = getValueWithForm(dict, "AttachmentMenu.SendItem", .one) - self._AttachmentMenu_SendItem_two = getValueWithForm(dict, "AttachmentMenu.SendItem", .two) - 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._MessageTimer_Weeks_zero = getValueWithForm(dict, "MessageTimer.Weeks", .zero) + self._MessageTimer_Weeks_one = getValueWithForm(dict, "MessageTimer.Weeks", .one) + self._MessageTimer_Weeks_two = getValueWithForm(dict, "MessageTimer.Weeks", .two) + self._MessageTimer_Weeks_few = getValueWithForm(dict, "MessageTimer.Weeks", .few) + self._MessageTimer_Weeks_many = getValueWithForm(dict, "MessageTimer.Weeks", .many) + self._MessageTimer_Weeks_other = getValueWithForm(dict, "MessageTimer.Weeks", .other) self._Contacts_ImportersCount_zero = getValueWithForm(dict, "Contacts.ImportersCount", .zero) self._Contacts_ImportersCount_one = getValueWithForm(dict, "Contacts.ImportersCount", .one) self._Contacts_ImportersCount_two = getValueWithForm(dict, "Contacts.ImportersCount", .two) self._Contacts_ImportersCount_few = getValueWithForm(dict, "Contacts.ImportersCount", .few) self._Contacts_ImportersCount_many = getValueWithForm(dict, "Contacts.ImportersCount", .many) self._Contacts_ImportersCount_other = getValueWithForm(dict, "Contacts.ImportersCount", .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) - self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) - self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) - self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) - self._LiveLocationUpdated_MinutesAgo_zero = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .zero) - self._LiveLocationUpdated_MinutesAgo_one = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .one) - self._LiveLocationUpdated_MinutesAgo_two = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .two) - self._LiveLocationUpdated_MinutesAgo_few = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .few) - self._LiveLocationUpdated_MinutesAgo_many = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .many) - self._LiveLocationUpdated_MinutesAgo_other = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .other) - self._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) - self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) - self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) - self._Call_ShortMinutes_few = getValueWithForm(dict, "Call.ShortMinutes", .few) - self._Call_ShortMinutes_many = getValueWithForm(dict, "Call.ShortMinutes", .many) - self._Call_ShortMinutes_other = getValueWithForm(dict, "Call.ShortMinutes", .other) - self._StickerPack_RemoveMaskCount_zero = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .zero) - self._StickerPack_RemoveMaskCount_one = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .one) - self._StickerPack_RemoveMaskCount_two = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .two) - self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) - self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) - self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) - self._Media_ShareItem_zero = getValueWithForm(dict, "Media.ShareItem", .zero) - self._Media_ShareItem_one = getValueWithForm(dict, "Media.ShareItem", .one) - self._Media_ShareItem_two = getValueWithForm(dict, "Media.ShareItem", .two) - self._Media_ShareItem_few = getValueWithForm(dict, "Media.ShareItem", .few) - self._Media_ShareItem_many = getValueWithForm(dict, "Media.ShareItem", .many) - self._Media_ShareItem_other = getValueWithForm(dict, "Media.ShareItem", .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._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_Generic_zero = getValueWithForm(dict, "SharedMedia.Generic", .zero) + self._SharedMedia_Generic_one = getValueWithForm(dict, "SharedMedia.Generic", .one) + self._SharedMedia_Generic_two = getValueWithForm(dict, "SharedMedia.Generic", .two) + self._SharedMedia_Generic_few = getValueWithForm(dict, "SharedMedia.Generic", .few) + self._SharedMedia_Generic_many = getValueWithForm(dict, "SharedMedia.Generic", .many) + self._SharedMedia_Generic_other = getValueWithForm(dict, "SharedMedia.Generic", .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._ForwardedLocations_zero = getValueWithForm(dict, "ForwardedLocations", .zero) self._ForwardedLocations_one = getValueWithForm(dict, "ForwardedLocations", .one) self._ForwardedLocations_two = getValueWithForm(dict, "ForwardedLocations", .two) self._ForwardedLocations_few = getValueWithForm(dict, "ForwardedLocations", .few) self._ForwardedLocations_many = getValueWithForm(dict, "ForwardedLocations", .many) self._ForwardedLocations_other = getValueWithForm(dict, "ForwardedLocations", .other) + self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) + self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) + self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) + self._Invitation_Members_few = getValueWithForm(dict, "Invitation.Members", .few) + self._Invitation_Members_many = getValueWithForm(dict, "Invitation.Members", .many) + self._Invitation_Members_other = getValueWithForm(dict, "Invitation.Members", .other) + self._DialogList_LiveLocationChatsCount_zero = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .zero) + self._DialogList_LiveLocationChatsCount_one = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .one) + self._DialogList_LiveLocationChatsCount_two = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .two) + self._DialogList_LiveLocationChatsCount_few = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .few) + self._DialogList_LiveLocationChatsCount_many = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .many) + self._DialogList_LiveLocationChatsCount_other = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .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) + self._ServiceMessage_GameScoreSelfSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .few) + self._ServiceMessage_GameScoreSelfSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .many) + self._ServiceMessage_GameScoreSelfSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .other) + self._Map_ETAMinutes_zero = getValueWithForm(dict, "Map.ETAMinutes", .zero) + self._Map_ETAMinutes_one = getValueWithForm(dict, "Map.ETAMinutes", .one) + self._Map_ETAMinutes_two = getValueWithForm(dict, "Map.ETAMinutes", .two) + 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._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) + self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) + self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) + self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .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._LiveLocationUpdated_MinutesAgo_zero = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .zero) + self._LiveLocationUpdated_MinutesAgo_one = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .one) + self._LiveLocationUpdated_MinutesAgo_two = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .two) + self._LiveLocationUpdated_MinutesAgo_few = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .few) + self._LiveLocationUpdated_MinutesAgo_many = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .many) + self._LiveLocationUpdated_MinutesAgo_other = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .other) + self._ForwardedStickers_zero = getValueWithForm(dict, "ForwardedStickers", .zero) + self._ForwardedStickers_one = getValueWithForm(dict, "ForwardedStickers", .one) + self._ForwardedStickers_two = getValueWithForm(dict, "ForwardedStickers", .two) + self._ForwardedStickers_few = getValueWithForm(dict, "ForwardedStickers", .few) + self._ForwardedStickers_many = getValueWithForm(dict, "ForwardedStickers", .many) + self._ForwardedStickers_other = getValueWithForm(dict, "ForwardedStickers", .other) + self._GroupInfo_ParticipantCount_zero = getValueWithForm(dict, "GroupInfo.ParticipantCount", .zero) + self._GroupInfo_ParticipantCount_one = getValueWithForm(dict, "GroupInfo.ParticipantCount", .one) + self._GroupInfo_ParticipantCount_two = getValueWithForm(dict, "GroupInfo.ParticipantCount", .two) + self._GroupInfo_ParticipantCount_few = getValueWithForm(dict, "GroupInfo.ParticipantCount", .few) + self._GroupInfo_ParticipantCount_many = getValueWithForm(dict, "GroupInfo.ParticipantCount", .many) + self._GroupInfo_ParticipantCount_other = getValueWithForm(dict, "GroupInfo.ParticipantCount", .other) + self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) + self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) + self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) + 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._Forward_ConfirmMultipleFiles_zero = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .zero) + self._Forward_ConfirmMultipleFiles_one = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .one) + self._Forward_ConfirmMultipleFiles_two = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .two) + self._Forward_ConfirmMultipleFiles_few = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .few) + self._Forward_ConfirmMultipleFiles_many = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .many) + self._Forward_ConfirmMultipleFiles_other = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .other) + self._Call_Seconds_zero = getValueWithForm(dict, "Call.Seconds", .zero) + self._Call_Seconds_one = getValueWithForm(dict, "Call.Seconds", .one) + self._Call_Seconds_two = getValueWithForm(dict, "Call.Seconds", .two) + self._Call_Seconds_few = getValueWithForm(dict, "Call.Seconds", .few) + self._Call_Seconds_many = getValueWithForm(dict, "Call.Seconds", .many) + self._Call_Seconds_other = getValueWithForm(dict, "Call.Seconds", .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) + self._PasscodeSettings_FailedAttempts_few = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .few) + self._PasscodeSettings_FailedAttempts_many = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .many) + self._PasscodeSettings_FailedAttempts_other = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .other) + self._ForwardedFiles_zero = getValueWithForm(dict, "ForwardedFiles", .zero) + self._ForwardedFiles_one = getValueWithForm(dict, "ForwardedFiles", .one) + self._ForwardedFiles_two = getValueWithForm(dict, "ForwardedFiles", .two) + self._ForwardedFiles_few = getValueWithForm(dict, "ForwardedFiles", .few) + self._ForwardedFiles_many = getValueWithForm(dict, "ForwardedFiles", .many) + self._ForwardedFiles_other = getValueWithForm(dict, "ForwardedFiles", .other) + self._MuteFor_Hours_zero = getValueWithForm(dict, "MuteFor.Hours", .zero) + self._MuteFor_Hours_one = getValueWithForm(dict, "MuteFor.Hours", .one) + self._MuteFor_Hours_two = getValueWithForm(dict, "MuteFor.Hours", .two) + self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) + self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) + self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) + self._AttachmentMenu_SendGif_zero = getValueWithForm(dict, "AttachmentMenu.SendGif", .zero) + self._AttachmentMenu_SendGif_one = getValueWithForm(dict, "AttachmentMenu.SendGif", .one) + self._AttachmentMenu_SendGif_two = getValueWithForm(dict, "AttachmentMenu.SendGif", .two) + self._AttachmentMenu_SendGif_few = getValueWithForm(dict, "AttachmentMenu.SendGif", .few) + self._AttachmentMenu_SendGif_many = getValueWithForm(dict, "AttachmentMenu.SendGif", .many) + self._AttachmentMenu_SendGif_other = getValueWithForm(dict, "AttachmentMenu.SendGif", .other) + self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) + self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) + self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) + self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) + self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) + self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) + self._LiveLocation_MenuChatsCount_zero = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .zero) + self._LiveLocation_MenuChatsCount_one = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .one) + self._LiveLocation_MenuChatsCount_two = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .two) + self._LiveLocation_MenuChatsCount_few = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .few) + self._LiveLocation_MenuChatsCount_many = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .many) + self._LiveLocation_MenuChatsCount_other = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .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._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) + self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) + self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) + self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) + self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) + self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .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) + self._MessageTimer_Years_few = getValueWithForm(dict, "MessageTimer.Years", .few) + self._MessageTimer_Years_many = getValueWithForm(dict, "MessageTimer.Years", .many) + self._MessageTimer_Years_other = getValueWithForm(dict, "MessageTimer.Years", .other) + self._MessageTimer_ShortDays_zero = getValueWithForm(dict, "MessageTimer.ShortDays", .zero) + self._MessageTimer_ShortDays_one = getValueWithForm(dict, "MessageTimer.ShortDays", .one) + self._MessageTimer_ShortDays_two = getValueWithForm(dict, "MessageTimer.ShortDays", .two) + self._MessageTimer_ShortDays_few = getValueWithForm(dict, "MessageTimer.ShortDays", .few) + self._MessageTimer_ShortDays_many = getValueWithForm(dict, "MessageTimer.ShortDays", .many) + self._MessageTimer_ShortDays_other = getValueWithForm(dict, "MessageTimer.ShortDays", .other) + self._InviteText_ContactsCount_zero = getValueWithForm(dict, "InviteText.ContactsCount", .zero) + self._InviteText_ContactsCount_one = getValueWithForm(dict, "InviteText.ContactsCount", .one) + self._InviteText_ContactsCount_two = getValueWithForm(dict, "InviteText.ContactsCount", .two) + self._InviteText_ContactsCount_few = getValueWithForm(dict, "InviteText.ContactsCount", .few) + self._InviteText_ContactsCount_many = getValueWithForm(dict, "InviteText.ContactsCount", .many) + self._InviteText_ContactsCount_other = getValueWithForm(dict, "InviteText.ContactsCount", .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._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) + self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) + self._MessageTimer_Seconds_two = getValueWithForm(dict, "MessageTimer.Seconds", .two) + self._MessageTimer_Seconds_few = getValueWithForm(dict, "MessageTimer.Seconds", .few) + self._MessageTimer_Seconds_many = getValueWithForm(dict, "MessageTimer.Seconds", .many) + self._MessageTimer_Seconds_other = getValueWithForm(dict, "MessageTimer.Seconds", .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) self._MessageTimer_Months_few = getValueWithForm(dict, "MessageTimer.Months", .few) self._MessageTimer_Months_many = getValueWithForm(dict, "MessageTimer.Months", .many) self._MessageTimer_Months_other = getValueWithForm(dict, "MessageTimer.Months", .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._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) + self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) + self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) + self._Call_ShortMinutes_few = getValueWithForm(dict, "Call.ShortMinutes", .few) + self._Call_ShortMinutes_many = getValueWithForm(dict, "Call.ShortMinutes", .many) + self._Call_ShortMinutes_other = getValueWithForm(dict, "Call.ShortMinutes", .other) + self._ForwardedPhotos_zero = getValueWithForm(dict, "ForwardedPhotos", .zero) + self._ForwardedPhotos_one = getValueWithForm(dict, "ForwardedPhotos", .one) + self._ForwardedPhotos_two = getValueWithForm(dict, "ForwardedPhotos", .two) + self._ForwardedPhotos_few = getValueWithForm(dict, "ForwardedPhotos", .few) + self._ForwardedPhotos_many = getValueWithForm(dict, "ForwardedPhotos", .many) + self._ForwardedPhotos_other = getValueWithForm(dict, "ForwardedPhotos", .other) + self._StickerPack_RemoveMaskCount_zero = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .zero) + self._StickerPack_RemoveMaskCount_one = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .one) + self._StickerPack_RemoveMaskCount_two = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .two) + self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) + self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) + self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) + self._Conversation_StatusSubscribers_zero = getValueWithForm(dict, "Conversation.StatusSubscribers", .zero) + self._Conversation_StatusSubscribers_one = getValueWithForm(dict, "Conversation.StatusSubscribers", .one) + self._Conversation_StatusSubscribers_two = getValueWithForm(dict, "Conversation.StatusSubscribers", .two) + self._Conversation_StatusSubscribers_few = getValueWithForm(dict, "Conversation.StatusSubscribers", .few) + self._Conversation_StatusSubscribers_many = getValueWithForm(dict, "Conversation.StatusSubscribers", .many) + self._Conversation_StatusSubscribers_other = getValueWithForm(dict, "Conversation.StatusSubscribers", .other) + self._ForwardedContacts_zero = getValueWithForm(dict, "ForwardedContacts", .zero) + self._ForwardedContacts_one = getValueWithForm(dict, "ForwardedContacts", .one) + self._ForwardedContacts_two = getValueWithForm(dict, "ForwardedContacts", .two) + self._ForwardedContacts_few = getValueWithForm(dict, "ForwardedContacts", .few) + self._ForwardedContacts_many = getValueWithForm(dict, "ForwardedContacts", .many) + self._ForwardedContacts_other = getValueWithForm(dict, "ForwardedContacts", .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) + self._Notification_GameScoreSelfSimple_few = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .few) + self._Notification_GameScoreSelfSimple_many = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .many) + self._Notification_GameScoreSelfSimple_other = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .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) + self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) + self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) + self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) + self._Media_SharePhoto_zero = getValueWithForm(dict, "Media.SharePhoto", .zero) + self._Media_SharePhoto_one = getValueWithForm(dict, "Media.SharePhoto", .one) + self._Media_SharePhoto_two = getValueWithForm(dict, "Media.SharePhoto", .two) + self._Media_SharePhoto_few = getValueWithForm(dict, "Media.SharePhoto", .few) + self._Media_SharePhoto_many = getValueWithForm(dict, "Media.SharePhoto", .many) + self._Media_SharePhoto_other = getValueWithForm(dict, "Media.SharePhoto", .other) + self._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) + self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) + self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) + self._AttachmentMenu_SendVideo_few = getValueWithForm(dict, "AttachmentMenu.SendVideo", .few) + self._AttachmentMenu_SendVideo_many = getValueWithForm(dict, "AttachmentMenu.SendVideo", .many) + self._AttachmentMenu_SendVideo_other = getValueWithForm(dict, "AttachmentMenu.SendVideo", .other) + self._SharedMedia_Photo_zero = getValueWithForm(dict, "SharedMedia.Photo", .zero) + self._SharedMedia_Photo_one = getValueWithForm(dict, "SharedMedia.Photo", .one) + self._SharedMedia_Photo_two = getValueWithForm(dict, "SharedMedia.Photo", .two) + self._SharedMedia_Photo_few = getValueWithForm(dict, "SharedMedia.Photo", .few) + self._SharedMedia_Photo_many = getValueWithForm(dict, "SharedMedia.Photo", .many) + self._SharedMedia_Photo_other = getValueWithForm(dict, "SharedMedia.Photo", .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._Media_ShareItem_zero = getValueWithForm(dict, "Media.ShareItem", .zero) + self._Media_ShareItem_one = getValueWithForm(dict, "Media.ShareItem", .one) + self._Media_ShareItem_two = getValueWithForm(dict, "Media.ShareItem", .two) + self._Media_ShareItem_few = getValueWithForm(dict, "Media.ShareItem", .few) + self._Media_ShareItem_many = getValueWithForm(dict, "Media.ShareItem", .many) + self._Media_ShareItem_other = getValueWithForm(dict, "Media.ShareItem", .other) + self._StickerPack_RemoveStickerCount_zero = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .zero) + self._StickerPack_RemoveStickerCount_one = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .one) + self._StickerPack_RemoveStickerCount_two = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .two) + self._StickerPack_RemoveStickerCount_few = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .few) + self._StickerPack_RemoveStickerCount_many = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .many) + self._StickerPack_RemoveStickerCount_other = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .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) + self._MessageTimer_ShortHours_few = getValueWithForm(dict, "MessageTimer.ShortHours", .few) + self._MessageTimer_ShortHours_many = getValueWithForm(dict, "MessageTimer.ShortHours", .many) + self._MessageTimer_ShortHours_other = getValueWithForm(dict, "MessageTimer.ShortHours", .other) + self._StickerPack_AddStickerCount_zero = getValueWithForm(dict, "StickerPack.AddStickerCount", .zero) + self._StickerPack_AddStickerCount_one = getValueWithForm(dict, "StickerPack.AddStickerCount", .one) + self._StickerPack_AddStickerCount_two = getValueWithForm(dict, "StickerPack.AddStickerCount", .two) + self._StickerPack_AddStickerCount_few = getValueWithForm(dict, "StickerPack.AddStickerCount", .few) + self._StickerPack_AddStickerCount_many = getValueWithForm(dict, "StickerPack.AddStickerCount", .many) + self._StickerPack_AddStickerCount_other = getValueWithForm(dict, "StickerPack.AddStickerCount", .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._StickerPack_AddMaskCount_zero = getValueWithForm(dict, "StickerPack.AddMaskCount", .zero) + self._StickerPack_AddMaskCount_one = getValueWithForm(dict, "StickerPack.AddMaskCount", .one) + self._StickerPack_AddMaskCount_two = getValueWithForm(dict, "StickerPack.AddMaskCount", .two) + 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._ForwardedMessages_zero = getValueWithForm(dict, "ForwardedMessages", .zero) + self._ForwardedMessages_one = getValueWithForm(dict, "ForwardedMessages", .one) + self._ForwardedMessages_two = getValueWithForm(dict, "ForwardedMessages", .two) + self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) + self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) + self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .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._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) + self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) + self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) + self._ForwardedAudios_few = getValueWithForm(dict, "ForwardedAudios", .few) + self._ForwardedAudios_many = getValueWithForm(dict, "ForwardedAudios", .many) + self._ForwardedAudios_other = getValueWithForm(dict, "ForwardedAudios", .other) + self._ForwardedVideoMessages_zero = getValueWithForm(dict, "ForwardedVideoMessages", .zero) + self._ForwardedVideoMessages_one = getValueWithForm(dict, "ForwardedVideoMessages", .one) + self._ForwardedVideoMessages_two = getValueWithForm(dict, "ForwardedVideoMessages", .two) + self._ForwardedVideoMessages_few = getValueWithForm(dict, "ForwardedVideoMessages", .few) + self._ForwardedVideoMessages_many = getValueWithForm(dict, "ForwardedVideoMessages", .many) + self._ForwardedVideoMessages_other = getValueWithForm(dict, "ForwardedVideoMessages", .other) + self._Watch_LastSeen_HoursAgo_zero = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .zero) + self._Watch_LastSeen_HoursAgo_one = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .one) + self._Watch_LastSeen_HoursAgo_two = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .two) + self._Watch_LastSeen_HoursAgo_few = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .few) + self._Watch_LastSeen_HoursAgo_many = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .many) + self._Watch_LastSeen_HoursAgo_other = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .other) + self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) + self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) + self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) + self._MessageTimer_ShortMinutes_few = getValueWithForm(dict, "MessageTimer.ShortMinutes", .few) + self._MessageTimer_ShortMinutes_many = getValueWithForm(dict, "MessageTimer.ShortMinutes", .many) + self._MessageTimer_ShortMinutes_other = getValueWithForm(dict, "MessageTimer.ShortMinutes", .other) self._MessageTimer_Days_zero = getValueWithForm(dict, "MessageTimer.Days", .zero) self._MessageTimer_Days_one = getValueWithForm(dict, "MessageTimer.Days", .one) self._MessageTimer_Days_two = getValueWithForm(dict, "MessageTimer.Days", .two) self._MessageTimer_Days_few = getValueWithForm(dict, "MessageTimer.Days", .few) self._MessageTimer_Days_many = getValueWithForm(dict, "MessageTimer.Days", .many) self._MessageTimer_Days_other = getValueWithForm(dict, "MessageTimer.Days", .other) + self._AttachmentMenu_SendPhoto_zero = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .zero) + self._AttachmentMenu_SendPhoto_one = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .one) + self._AttachmentMenu_SendPhoto_two = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .two) + 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._Conversation_StatusOnline_zero = getValueWithForm(dict, "Conversation.StatusOnline", .zero) + self._Conversation_StatusOnline_one = getValueWithForm(dict, "Conversation.StatusOnline", .one) + self._Conversation_StatusOnline_two = getValueWithForm(dict, "Conversation.StatusOnline", .two) + self._Conversation_StatusOnline_few = getValueWithForm(dict, "Conversation.StatusOnline", .few) + self._Conversation_StatusOnline_many = getValueWithForm(dict, "Conversation.StatusOnline", .many) + self._Conversation_StatusOnline_other = getValueWithForm(dict, "Conversation.StatusOnline", .other) + self._ServiceMessage_GameScoreSelfExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .zero) + self._ServiceMessage_GameScoreSelfExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .one) + self._ServiceMessage_GameScoreSelfExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .two) + self._ServiceMessage_GameScoreSelfExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .few) + self._ServiceMessage_GameScoreSelfExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .many) + self._ServiceMessage_GameScoreSelfExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .other) + self._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) + self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) + self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) + self._MuteExpires_Hours_few = getValueWithForm(dict, "MuteExpires.Hours", .few) + self._MuteExpires_Hours_many = getValueWithForm(dict, "MuteExpires.Hours", .many) + self._MuteExpires_Hours_other = getValueWithForm(dict, "MuteExpires.Hours", .other) + self._ServiceMessage_GameScoreExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .zero) + self._ServiceMessage_GameScoreExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .one) + self._ServiceMessage_GameScoreExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .two) + self._ServiceMessage_GameScoreExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .few) + self._ServiceMessage_GameScoreExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .many) + self._ServiceMessage_GameScoreExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .other) + self._ServiceMessage_GameScoreSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .zero) + self._ServiceMessage_GameScoreSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .one) + self._ServiceMessage_GameScoreSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .two) + self._ServiceMessage_GameScoreSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .few) + self._ServiceMessage_GameScoreSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .many) + self._ServiceMessage_GameScoreSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .other) + self._Conversation_LiveLocationMembersCount_zero = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .zero) + self._Conversation_LiveLocationMembersCount_one = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .one) + self._Conversation_LiveLocationMembersCount_two = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .two) + self._Conversation_LiveLocationMembersCount_few = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .few) + self._Conversation_LiveLocationMembersCount_many = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .many) + self._Conversation_LiveLocationMembersCount_other = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .other) + self._Call_ShortSeconds_zero = getValueWithForm(dict, "Call.ShortSeconds", .zero) + self._Call_ShortSeconds_one = getValueWithForm(dict, "Call.ShortSeconds", .one) + self._Call_ShortSeconds_two = getValueWithForm(dict, "Call.ShortSeconds", .two) + 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._QuickSend_Photos_zero = getValueWithForm(dict, "QuickSend.Photos", .zero) + self._QuickSend_Photos_one = getValueWithForm(dict, "QuickSend.Photos", .one) + self._QuickSend_Photos_two = getValueWithForm(dict, "QuickSend.Photos", .two) + self._QuickSend_Photos_few = getValueWithForm(dict, "QuickSend.Photos", .few) + self._QuickSend_Photos_many = getValueWithForm(dict, "QuickSend.Photos", .many) + self._QuickSend_Photos_other = getValueWithForm(dict, "QuickSend.Photos", .other) self._MuteExpires_Minutes_zero = getValueWithForm(dict, "MuteExpires.Minutes", .zero) self._MuteExpires_Minutes_one = getValueWithForm(dict, "MuteExpires.Minutes", .one) self._MuteExpires_Minutes_two = getValueWithForm(dict, "MuteExpires.Minutes", .two) self._MuteExpires_Minutes_few = getValueWithForm(dict, "MuteExpires.Minutes", .few) self._MuteExpires_Minutes_many = getValueWithForm(dict, "MuteExpires.Minutes", .many) self._MuteExpires_Minutes_other = getValueWithForm(dict, "MuteExpires.Minutes", .other) + self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) + self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) + self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) + self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) + self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) + self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) + self._Media_ShareVideo_zero = getValueWithForm(dict, "Media.ShareVideo", .zero) + self._Media_ShareVideo_one = getValueWithForm(dict, "Media.ShareVideo", .one) + self._Media_ShareVideo_two = getValueWithForm(dict, "Media.ShareVideo", .two) + self._Media_ShareVideo_few = getValueWithForm(dict, "Media.ShareVideo", .few) + self._Media_ShareVideo_many = getValueWithForm(dict, "Media.ShareVideo", .many) + self._Media_ShareVideo_other = getValueWithForm(dict, "Media.ShareVideo", .other) + self._StickerPack_StickerCount_zero = getValueWithForm(dict, "StickerPack.StickerCount", .zero) + self._StickerPack_StickerCount_one = getValueWithForm(dict, "StickerPack.StickerCount", .one) + self._StickerPack_StickerCount_two = getValueWithForm(dict, "StickerPack.StickerCount", .two) + 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._AttachmentMenu_SendItem_zero = getValueWithForm(dict, "AttachmentMenu.SendItem", .zero) + self._AttachmentMenu_SendItem_one = getValueWithForm(dict, "AttachmentMenu.SendItem", .one) + self._AttachmentMenu_SendItem_two = getValueWithForm(dict, "AttachmentMenu.SendItem", .two) + 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._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_GameScoreSelfExtended_zero = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .zero) + self._Notification_GameScoreSelfExtended_one = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .one) + self._Notification_GameScoreSelfExtended_two = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .two) + self._Notification_GameScoreSelfExtended_few = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .few) + self._Notification_GameScoreSelfExtended_many = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .many) + self._Notification_GameScoreSelfExtended_other = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .other) + self._SharedMedia_Link_zero = getValueWithForm(dict, "SharedMedia.Link", .zero) + self._SharedMedia_Link_one = getValueWithForm(dict, "SharedMedia.Link", .one) + self._SharedMedia_Link_two = getValueWithForm(dict, "SharedMedia.Link", .two) + self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) + self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) + self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .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._Call_Minutes_zero = getValueWithForm(dict, "Call.Minutes", .zero) + self._Call_Minutes_one = getValueWithForm(dict, "Call.Minutes", .one) + self._Call_Minutes_two = getValueWithForm(dict, "Call.Minutes", .two) + self._Call_Minutes_few = getValueWithForm(dict, "Call.Minutes", .few) + self._Call_Minutes_many = getValueWithForm(dict, "Call.Minutes", .many) + self._Call_Minutes_other = getValueWithForm(dict, "Call.Minutes", .other) + self._MessageTimer_ShortSeconds_zero = getValueWithForm(dict, "MessageTimer.ShortSeconds", .zero) + self._MessageTimer_ShortSeconds_one = getValueWithForm(dict, "MessageTimer.ShortSeconds", .one) + self._MessageTimer_ShortSeconds_two = getValueWithForm(dict, "MessageTimer.ShortSeconds", .two) + 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._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) + self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) + self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) + self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) } } diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 6233511d4b..964aa4d95d 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -233,12 +233,16 @@ public final class PresentationThemeItemDisclosureActions { public let neutral2: PresentationThemeItemDisclosureAction public let destructive: PresentationThemeItemDisclosureAction public let constructive: PresentationThemeItemDisclosureAction + public let accent: PresentationThemeItemDisclosureAction + public let warning: PresentationThemeItemDisclosureAction - public init(neutral1: PresentationThemeItemDisclosureAction, neutral2: PresentationThemeItemDisclosureAction, destructive: PresentationThemeItemDisclosureAction, constructive: PresentationThemeItemDisclosureAction) { + public init(neutral1: PresentationThemeItemDisclosureAction, neutral2: PresentationThemeItemDisclosureAction, destructive: PresentationThemeItemDisclosureAction, constructive: PresentationThemeItemDisclosureAction, accent: PresentationThemeItemDisclosureAction, warning: PresentationThemeItemDisclosureAction) { self.neutral1 = neutral1 self.neutral2 = neutral2 self.destructive = destructive self.constructive = constructive + self.accent = accent + self.warning = warning } } @@ -272,13 +276,14 @@ public final class PresentationThemeList { public let freeTextColor: UIColor public let freeTextErrorColor: UIColor public let freeTextSuccessColor: UIColor + public let freeMonoIcon: UIColor public let itemSwitchColors: PresentationThemeSwitch public let itemDisclosureActions: PresentationThemeItemDisclosureActions public let itemCheckColors: PresentationThemeCheck public let controlSecondaryColor: UIColor public let freeInputField: PresentationInputFieldTheme - public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeCheck, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme) { + public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, freeMonoIcon: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeCheck, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme) { self.blocksBackgroundColor = blocksBackgroundColor self.plainBackgroundColor = plainBackgroundColor self.itemPrimaryTextColor = itemPrimaryTextColor @@ -296,6 +301,7 @@ public final class PresentationThemeList { self.freeTextColor = freeTextColor self.freeTextErrorColor = freeTextErrorColor self.freeTextSuccessColor = freeTextSuccessColor + self.freeMonoIcon = freeMonoIcon self.itemSwitchColors = itemSwitchColors self.itemDisclosureActions = itemDisclosureActions self.itemCheckColors = itemCheckColors diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index 12d7b29d1c..1083575e18 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -1,5 +1,6 @@ import Foundation import Postbox +import TelegramCore import SwiftSignalKit public enum PresentationBuiltinThemeReference: Int32 { @@ -100,8 +101,8 @@ public struct PresentationThemeSettings: PreferencesEntry { } public func updatePresentationThemeSettingsInteractively(postbox: Postbox, _ f: @escaping (PresentationThemeSettings) -> PresentationThemeSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings, { entry in let currentSettings: PresentationThemeSettings if let entry = entry as? PresentationThemeSettings { currentSettings = entry diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index ac45455d9b..1be877ed3d 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -15,8 +15,11 @@ private final class PrivacyAndSecurityControllerArguments { let openActiveSessions: () -> Void let setupAccountAutoremove: () -> Void let clearPaymentInfo: () -> Void + let updateSecretChatLinkPreviews: (Bool) -> Void + let deleteContacts: () -> Void + let updateSyncContacts: (Bool) -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, clearPaymentInfo: @escaping () -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -27,6 +30,9 @@ private final class PrivacyAndSecurityControllerArguments { self.openActiveSessions = openActiveSessions self.setupAccountAutoremove = setupAccountAutoremove self.clearPaymentInfo = clearPaymentInfo + self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews + self.deleteContacts = deleteContacts + self.updateSyncContacts = updateSyncContacts } } @@ -35,6 +41,8 @@ private enum PrivacyAndSecuritySection: Int32 { case security case account case payment + case secretChatLinkPreviews + case contacts } private enum PrivacyAndSecurityEntry: ItemListNodeEntry { @@ -53,6 +61,13 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case paymentHeader(PresentationTheme, String) case clearPaymentInfo(PresentationTheme, String, Bool) case paymentInfo(PresentationTheme, String) + case secretChatLinkPreviewsHeader(PresentationTheme, String) + case secretChatLinkPreviews(PresentationTheme, String, Bool) + case secretChatLinkPreviewsInfo(PresentationTheme, String) + case contactsHeader(PresentationTheme, String) + case deleteContacts(PresentationTheme, String, Bool) + case syncContacts(PresentationTheme, String, Bool) + case syncContactsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -64,6 +79,10 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return PrivacyAndSecuritySection.account.rawValue case .paymentHeader, .clearPaymentInfo, .paymentInfo: return PrivacyAndSecuritySection.payment.rawValue + case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: + return PrivacyAndSecuritySection.secretChatLinkPreviews.rawValue + case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: + return PrivacyAndSecuritySection.contacts.rawValue } } @@ -99,6 +118,20 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 13 case .paymentInfo: return 14 + case .secretChatLinkPreviewsHeader: + return 15 + case .secretChatLinkPreviews: + return 16 + case .secretChatLinkPreviewsInfo: + return 17 + case .contactsHeader: + return 18 + case .deleteContacts: + return 19 + case .syncContacts: + return 20 + case .syncContactsInfo: + return 21 } } @@ -194,6 +227,48 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText): + if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled): + if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText): + if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .contactsHeader(lhsTheme, lhsText): + if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .deleteContacts(lhsTheme, lhsText, lhsEnabled): + if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .syncContacts(lhsTheme, lhsText, lhsEnabled): + if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .syncContactsInfo(lhsTheme, lhsText): + if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -251,6 +326,26 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }) case let .paymentInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .secretChatLinkPreviewsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .secretChatLinkPreviews(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateSecretChatLinkPreviews(updatedValue) + }) + case let .secretChatLinkPreviewsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .contactsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .deleteContacts(theme, text, value): + return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.deleteContacts() + }) + case let .syncContacts(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateSyncContacts(updatedValue) + }) + case let .syncContactsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -259,11 +354,13 @@ private struct PrivacyAndSecurityControllerState: Equatable { let updatingAccountTimeoutValue: Int32? let clearingPaymentInfo: Bool let clearedPaymentInfo: Bool + let deletingContacts: Bool - init(updatingAccountTimeoutValue: Int32? = nil, clearingPaymentInfo: Bool = false, clearedPaymentInfo: Bool = false) { + init(updatingAccountTimeoutValue: Int32? = nil, clearingPaymentInfo: Bool = false, clearedPaymentInfo: Bool = false, deletingContacts: Bool = false) { self.updatingAccountTimeoutValue = updatingAccountTimeoutValue self.clearingPaymentInfo = clearingPaymentInfo self.clearedPaymentInfo = clearedPaymentInfo + self.deletingContacts = deletingContacts } static func ==(lhs: PrivacyAndSecurityControllerState, rhs: PrivacyAndSecurityControllerState) -> Bool { @@ -276,20 +373,27 @@ private struct PrivacyAndSecurityControllerState: Equatable { if lhs.clearedPaymentInfo != rhs.clearedPaymentInfo { return false } + if lhs.deletingContacts != rhs.deletingContacts { + return false + } return true } func withUpdatedUpdatingAccountTimeoutValue(_ updatingAccountTimeoutValue: Int32?) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo) + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: self.deletingContacts) } func withUpdatedClearingPaymentInfo(_ clearingPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo) + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: self.deletingContacts) } func withUpdatedClearedPaymentInfo(_ clearedPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: clearedPaymentInfo) + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: clearedPaymentInfo, deletingContacts: self.deletingContacts) + } + + func withUpdatedDeletingContacts(_ deletingContacts: Bool) -> PrivacyAndSecurityControllerState { + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: deletingContacts) } } @@ -320,7 +424,7 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings: } } -private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) @@ -370,6 +474,15 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp)) } + entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, "SECRET CHATS")) + entries.append(.secretChatLinkPreviews(presentationData.theme, "Link previews", secretChatLinkPreviews ?? true)) + entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, "Link previews will be generated on Telegram servers. We do not store data about the links you send.")) + + entries.append(.contactsHeader(presentationData.theme, "CONTACTS")) + entries.append(.deleteContacts(presentationData.theme, "Delete Synced Contacts", !state.deletingContacts)) + entries.append(.syncContacts(presentationData.theme, "Sync Contacts", synchronizeDeviceContacts)) + entries.append(.syncContactsInfo(presentationData.theme, "Turn on to continuously sync contacts from this device with your account.")) + return entries } @@ -571,12 +684,68 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) + }, updateSecretChatLinkPreviews: { value in + let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(postbox: account.postbox, value: value).start() + }, deleteContacts: { + var canBegin = false + updateState { state in + if !state.deletingContacts { + canBegin = true + } + return state + } + if canBegin { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "This will remove your contacts from the Telegram servers.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + var begin = false + updateState { state in + var state = state + if !state.deletingContacts { + state = state.withUpdatedDeletingContacts(true) + begin = true + } + return state + } + + if !begin { + return + } + + let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in + var settings = settings + settings.synchronizeDeviceContacts = false + return settings + }) + + actionsDisposable.add((deleteAllContacts(postbox: account.postbox, network: account.network) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state = state.withUpdatedDeletingContacts(false) + return state + } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "All your contacts were deleted from the server.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + })) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) + } + }, updateSyncContacts: { value in + let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in + var settings = settings + settings.synchronizeDeviceContacts = value + return settings + }).start() }) let previousState = Atomic(value: nil) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get()) - |> map { presentationData, state, privacySettings -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in + let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings])) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get(), account.postbox.combinedView(keys: [.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey()), preferencesKey])) + |> map { presentationData, state, privacySettings, combined -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in + let secretChatLinkPreviews = (combined.views[.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey())] as? NoticeEntryView)?.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) }) + + let synchronizeDeviceContacts: Bool = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)?.synchronizeDeviceContacts ?? true var rightNavigationButton: ItemListNavigationButton? if privacySettings == nil || state.updatingAccountTimeoutValue != nil { @@ -593,7 +762,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign } } - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ProxyListSettingsController.swift b/TelegramUI/ProxyListSettingsController.swift index 284a4fa31f..6696d33bab 100644 --- a/TelegramUI/ProxyListSettingsController.swift +++ b/TelegramUI/ProxyListSettingsController.swift @@ -44,7 +44,7 @@ private struct DisplayProxyServerStatus: Equatable { private enum ProxySettingsControllerEntryId: Equatable, Hashable { case index(Int) - case server(String, Int32, String, String) + case server(String, Int32, ProxyServerConnection) } private enum ProxySettingsControllerEntry: ItemListNodeEntry { @@ -75,7 +75,7 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { case .addServer: return .index(2) case let .server(_, _, _, settings, _, _, _): - return .server(settings.host, settings.port, settings.username ?? "", settings.password ?? "") + return .server(settings.host, settings.port, settings.connection) case .useForCalls: return .index(3) case .useForCallsInfo: @@ -208,8 +208,8 @@ private func proxySettingsControllerEntries(presentationData: PresentationData, var entries: [ProxySettingsControllerEntry] = [] entries.append(.enabled(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_UseProxy, proxySettings.enabled, proxySettings.servers.isEmpty)) - entries.append(.serversHeader(presentationData.theme, "SAVED PROXIES")) - entries.append(.addServer(presentationData.theme, "Add Proxy", state.editing)) + entries.append(.serversHeader(presentationData.theme, presentationData.strings.SocksProxySetup_SavedProxies)) + entries.append(.addServer(presentationData.theme, presentationData.strings.SocksProxySetup_AddProxy, state.editing)) var index = 0 for server in proxySettings.servers { let status: ProxyServerStatus = statuses[server] ?? .checking @@ -217,29 +217,36 @@ private func proxySettingsControllerEntries(presentationData: PresentationData, if proxySettings.enabled && server == proxySettings.activeServer { switch connectionStatus { case .waitingForNetwork: - displayStatus = DisplayProxyServerStatus(activity: true, text: "waiting for network", textActive: false) + displayStatus = DisplayProxyServerStatus(activity: true, text: presentationData.strings.State_WaitingForNetwork.lowercased(), textActive: false) case .connecting, .updating: - displayStatus = DisplayProxyServerStatus(activity: true, text: "connecting", textActive: false) + displayStatus = DisplayProxyServerStatus(activity: true, text: presentationData.strings.SocksProxySetup_ProxyStatusConnecting, textActive: false) case .online: - displayStatus = DisplayProxyServerStatus(activity: false, text: "online", textActive: true) + var text = presentationData.strings.SocksProxySetup_ProxyStatusConnected + if case let .available(rtt) = status { + let pingTime: Int = Int(rtt * 1000.0) + text = text + ", \(presentationData.strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").0)" + } + displayStatus = DisplayProxyServerStatus(activity: false, text: text, textActive: true) } } else { switch status { case .notAvailable: - displayStatus = DisplayProxyServerStatus(activity: false, text: "not available", textActive: false) + displayStatus = DisplayProxyServerStatus(activity: false, text: presentationData.strings.SocksProxySetup_ProxyStatusUnavailable, textActive: false) case .checking: - displayStatus = DisplayProxyServerStatus(activity: false, text: "checking", textActive: false) + displayStatus = DisplayProxyServerStatus(activity: false, text: presentationData.strings.SocksProxySetup_ProxyStatusChecking, textActive: false) case let .available(rtt): let pingTime: Int = Int(rtt * 1000.0) - displayStatus = DisplayProxyServerStatus(activity: false, text: "available (ping: \(pingTime) ms)", textActive: false) + displayStatus = DisplayProxyServerStatus(activity: false, text: presentationData.strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").0, textActive: false) } } entries.append(.server(index, presentationData.theme, presentationData.strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server))) index += 1 } - entries.append(.useForCalls(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCalls, proxySettings.useForCalls)) - entries.append(.useForCallsInfo(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCallsHelp)) + if let activeServer = proxySettings.activeServer, case .socks5 = activeServer.connection { + entries.append(.useForCalls(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCalls, proxySettings.useForCalls)) + entries.append(.useForCallsInfo(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCallsHelp)) + } return entries } @@ -326,7 +333,7 @@ public func proxySettingsController(account: Account) -> ViewController { } }) - let statusesContext = ProxyServersStatuses(account: account, servers: proxySettings.get() + let statusesContext = ProxyServersStatuses(network: account.network, servers: proxySettings.get() |> map { proxySettings -> [ProxyServerSettings] in return proxySettings.servers }) diff --git a/TelegramUI/ProxyServerActionSheetController.swift b/TelegramUI/ProxyServerActionSheetController.swift index d0a4371bb4..9fe880d4c0 100644 --- a/TelegramUI/ProxyServerActionSheetController.swift +++ b/TelegramUI/ProxyServerActionSheetController.swift @@ -98,28 +98,41 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode { portTextNode.attributedText = NSAttributedString(string: "\(server.port)", font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((portTitleNode, portTextNode)) - if let username = server.username { - let usernameTitleNode = ImmediateTextNode() - usernameTitleNode.isLayerBacked = true - usernameTitleNode.displaysAsynchronously = false - usernameTitleNode.attributedText = NSAttributedString(string: "Username", font: textFont, textColor: theme.secondaryTextColor) - let usernameTextNode = ImmediateTextNode() - usernameTextNode.isLayerBacked = true - usernameTextNode.displaysAsynchronously = false - usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((usernameTitleNode, usernameTextNode)) - } - - if let password = server.password { - let passwordTitleNode = ImmediateTextNode() - passwordTitleNode.isLayerBacked = true - passwordTitleNode.displaysAsynchronously = false - passwordTitleNode.attributedText = NSAttributedString(string: "Password", font: textFont, textColor: theme.secondaryTextColor) - let passwordTextNode = ImmediateTextNode() - passwordTextNode.isLayerBacked = true - passwordTextNode.displaysAsynchronously = false - passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((passwordTitleNode, passwordTextNode)) + switch server.connection { + case let .socks5(username, password): + if let username = username { + let usernameTitleNode = ImmediateTextNode() + usernameTitleNode.isLayerBacked = true + usernameTitleNode.displaysAsynchronously = false + usernameTitleNode.attributedText = NSAttributedString(string: "Username", font: textFont, textColor: theme.secondaryTextColor) + let usernameTextNode = ImmediateTextNode() + usernameTextNode.isLayerBacked = true + usernameTextNode.displaysAsynchronously = false + usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((usernameTitleNode, usernameTextNode)) + } + + if let password = password { + let passwordTitleNode = ImmediateTextNode() + passwordTitleNode.isLayerBacked = true + passwordTitleNode.displaysAsynchronously = false + passwordTitleNode.attributedText = NSAttributedString(string: "Password", font: textFont, textColor: theme.secondaryTextColor) + let passwordTextNode = ImmediateTextNode() + passwordTextNode.isLayerBacked = true + passwordTextNode.displaysAsynchronously = false + passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((passwordTitleNode, passwordTextNode)) + } + case let .mtp(secret): + let passwordTitleNode = ImmediateTextNode() + passwordTitleNode.isLayerBacked = true + passwordTitleNode.displaysAsynchronously = false + passwordTitleNode.attributedText = NSAttributedString(string: "Secret", font: textFont, textColor: theme.secondaryTextColor) + let passwordTextNode = ImmediateTextNode() + passwordTextNode.isLayerBacked = true + passwordTextNode.displaysAsynchronously = false + passwordTextNode.attributedText = NSAttributedString(string: "•••••", font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((passwordTitleNode, passwordTextNode)) } self.fieldNodes = fieldNodes @@ -260,9 +273,9 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { @objc private func buttonPressed() { let proxyServerSettings = self.server let network = self.account.network - let _ = (self.account.postbox.modify { modifier -> ProxySettings in + let _ = (self.account.postbox.transaction { transaction -> ProxySettings in var currentSettings: ProxySettings? - updateProxySettingsInteractively(modifier: modifier, network: network, { settings in + updateProxySettingsInteractively(transaction: transaction, network: network, { settings in currentSettings = settings var settings = settings if let index = settings.servers.index(of: proxyServerSettings) { diff --git a/TelegramUI/ProxyServerSettingsController.swift b/TelegramUI/ProxyServerSettingsController.swift index 78d53b4cff..f4411c8eb8 100644 --- a/TelegramUI/ProxyServerSettingsController.swift +++ b/TelegramUI/ProxyServerSettingsController.swift @@ -3,24 +3,29 @@ import Display import SwiftSignalKit import Postbox import TelegramCore +import MtProtoKitDynamic private final class proxyServerSettingsControllerArguments { - let updateState: ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void + let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let share: () -> Void - init(updateState: @escaping ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void, share: @escaping () -> Void) { + init(updateState: @escaping ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void, share: @escaping () -> Void) { self.updateState = updateState self.share = share } } private enum ProxySettingsSection: Int32 { + case mode case connection case credentials case share } private enum ProxySettingsEntry: ItemListNodeEntry { + case modeSocks5(PresentationTheme, String, Bool) + case modeMtp(PresentationTheme, String, Bool) + case connectionHeader(PresentationTheme, String) case connectionServer(PresentationTheme, String, String) case connectionPort(PresentationTheme, String, String) @@ -28,14 +33,17 @@ private enum ProxySettingsEntry: ItemListNodeEntry { case credentialsHeader(PresentationTheme, String) case credentialsUsername(PresentationTheme, String, String) case credentialsPassword(PresentationTheme, String, String) + case credentialsSecret(PresentationTheme, String, String) case share(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { + case .modeSocks5, .modeMtp: + return ProxySettingsSection.mode.rawValue case .connectionHeader, .connectionServer, .connectionPort: return ProxySettingsSection.connection.rawValue - case .credentialsHeader, .credentialsUsername, .credentialsPassword: + case .credentialsHeader, .credentialsUsername, .credentialsPassword, .credentialsSecret: return ProxySettingsSection.credentials.rawValue case .share: return ProxySettingsSection.share.rawValue @@ -44,6 +52,10 @@ private enum ProxySettingsEntry: ItemListNodeEntry { var stableId: Int32 { switch self { + case .modeSocks5: + return 0 + case .modeMtp: + return 1 case .connectionHeader: return 2 case .connectionServer: @@ -56,13 +68,27 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return 6 case .credentialsPassword: return 7 - case .share: + case .credentialsSecret: return 8 + case .share: + return 9 } } static func ==(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { switch lhs { + case let .modeSocks5(lhsTheme, lhsText, lhsValue): + if case let .modeSocks5(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .modeMtp(lhsTheme, lhsText, lhsValue): + if case let .modeMtp(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .connectionHeader(lhsTheme, lhsText): if case let .connectionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -99,6 +125,12 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } else { return false } + case let .credentialsSecret(lhsTheme, lhsText, lhsValue): + if case let .credentialsSecret(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .share(lhsTheme, lhsText, lhsValue): if case let .share(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -114,6 +146,22 @@ private enum ProxySettingsEntry: ItemListNodeEntry { func item(_ arguments: proxyServerSettingsControllerArguments) -> ListViewItem { switch self { + case let .modeSocks5(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateState { state in + var state = state + state.mode = .socks5 + return state + } + }) + case let .modeMtp(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateState { state in + var state = state + state.mode = .mtp + return state + } + }) case let .connectionHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .connectionServer(theme, placeholder, text): @@ -150,6 +198,14 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return state } }, action: {}) + case let .credentialsSecret(theme, placeholder, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular, sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.secret = value + return state + } + }, action: {}) case let .share(theme, text, enabled): return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.share() @@ -158,30 +214,61 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } } -private struct proxyServerSettingsControllerState: Equatable { +private enum ProxyServerSettingsControllerMode { + case socks5 + case mtp +} + +private struct ProxyServerSettingsControllerState: Equatable { + var mode: ProxyServerSettingsControllerMode var host: String var port: String var username: String var password: String + var secret: String var isComplete: Bool { if self.host.isEmpty || self.port.isEmpty || Int(self.port) == nil { return false } + switch self.mode { + case .socks5: + break + case .mtp: + let data = dataWithHexString(self.secret) + var secretIsValid = false + if data.count == 16 { + secretIsValid = true + } else if data.count == 17 && MTSocksProxySettings.secretSupportsExtendedPadding(data) { + secretIsValid = true + } + if !secretIsValid { + return false + } + } return true } } -private func proxyServerSettingsControllerEntries(presentationData: PresentationData, state: proxyServerSettingsControllerState) -> [ProxySettingsEntry] { +private func proxyServerSettingsControllerEntries(presentationData: PresentationData, state: ProxyServerSettingsControllerState) -> [ProxySettingsEntry] { var entries: [ProxySettingsEntry] = [] + entries.append(.modeSocks5(presentationData.theme, presentationData.strings.SocksProxySetup_ProxySocks5, state.mode == .socks5)) + entries.append(.modeMtp(presentationData.theme, presentationData.strings.SocksProxySetup_ProxyTelegram, state.mode == .mtp)) + entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased())) entries.append(.connectionServer(presentationData.theme, presentationData.strings.SocksProxySetup_Hostname, state.host)) entries.append(.connectionPort(presentationData.theme, presentationData.strings.SocksProxySetup_Port, state.port)) - entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) - entries.append(.credentialsUsername(presentationData.theme, presentationData.strings.SocksProxySetup_Username, state.username)) - entries.append(.credentialsPassword(presentationData.theme, presentationData.strings.SocksProxySetup_Password, state.password)) + switch state.mode { + case .socks5: + entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) + entries.append(.credentialsUsername(presentationData.theme, presentationData.strings.SocksProxySetup_Username, state.username)) + entries.append(.credentialsPassword(presentationData.theme, presentationData.strings.SocksProxySetup_Password, state.password)) + case .mtp: + entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_RequiredCredentials)) + entries.append(.credentialsSecret(presentationData.theme, presentationData.strings.SocksProxySetup_SecretPlaceholder, state.secret)) + } entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete)) @@ -189,10 +276,25 @@ private func proxyServerSettingsControllerEntries(presentationData: Presentation } func proxyServerSettingsController(account: Account, currentSettings: ProxyServerSettings?) -> ViewController { - let initialState = proxyServerSettingsControllerState(host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentSettings?.username ?? "", password: currentSettings?.password ?? "") + var currentMode: ProxyServerSettingsControllerMode = .socks5 + var currentUsername: String? + var currentPassword: String? + var currentSecret: String? + if let currentSettings = currentSettings { + switch currentSettings.connection { + case let .socks5(username, password): + currentUsername = username + currentPassword = password + currentMode = .socks5 + case let .mtp(secret): + currentSecret = hexString(secret) + currentMode = .mtp + } + } + let initialState = ProxyServerSettingsControllerState(mode: currentMode, host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentUsername ?? "", password: currentPassword ?? "", secret: currentSecret ?? "") let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let updateState: ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void = { f in + let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -205,9 +307,16 @@ func proxyServerSettingsController(account: Account, currentSettings: ProxyServe let state = stateValue.with { $0 } if state.isComplete { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - var result = "tg://socks?server=\(state.host)&port=\(state.port)" - if !state.username.isEmpty { - result += "&user=\((state.username as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\((state.password as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + var result: String + switch state.mode { + case .mtp: + result = "tg://proxy?server=\(state.host)&port=\(state.port)" + result += "&secret=\((state.secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + case .socks5: + result = "tg://socks?server=\(state.host)&port=\(state.port)" + if !state.username.isEmpty { + result += "&user=\((state.username as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\((state.password as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } } UIPasteboard.general.string = result @@ -224,7 +333,21 @@ func proxyServerSettingsController(account: Account, currentSettings: ProxyServe let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { var proxyServerSettings: ProxyServerSettings? if state.isComplete, let port = Int32(state.port) { - proxyServerSettings = ProxyServerSettings(host: state.host, port: port, username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password) + switch state.mode { + case .socks5: + proxyServerSettings = ProxyServerSettings(host: state.host, port: port, connection: .socks5(username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password)) + case .mtp: + let data = dataWithHexString(state.secret) + var secretIsValid = false + if data.count == 16 { + secretIsValid = true + } else if data.count == 17 && MTSocksProxySettings.secretSupportsExtendedPadding(data) { + secretIsValid = true + } + if secretIsValid { + proxyServerSettings = ProxyServerSettings(host: state.host, port: port, connection: .mtp(secret: data)) + } + } } if let proxyServerSettings = proxyServerSettings { let _ = (updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { settings in diff --git a/TelegramUI/ProxySettingsServerItem.swift b/TelegramUI/ProxySettingsServerItem.swift index b9e90368ab..707aac41ee 100644 --- a/TelegramUI/ProxySettingsServerItem.swift +++ b/TelegramUI/ProxySettingsServerItem.swift @@ -370,7 +370,7 @@ class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions(peerRevealOptions) + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } }) diff --git a/TelegramUI/RenderedTotalUnreadCount.swift b/TelegramUI/RenderedTotalUnreadCount.swift index 69cda6bc61..4c83f58476 100644 --- a/TelegramUI/RenderedTotalUnreadCount.swift +++ b/TelegramUI/RenderedTotalUnreadCount.swift @@ -2,25 +2,50 @@ import Foundation import Postbox import SwiftSignalKit -public func renderedTotalUnreadCount(postbox: Postbox) -> Signal { - let unreadCountsKey = PostboxViewKey.unreadCounts(items: [UnreadMessageCountsItem.total(.raw)]) +public enum RenderedTotalUnreadCountType { + case raw + case filtered +} + +public func renderedTotalUnreadCount(transaction: Transaction) -> (Int32, RenderedTotalUnreadCountType) { + let totalUnreadState = transaction.getTotalUnreadState() + let inAppSettings: InAppNotificationSettings = (transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings) ?? .defaultSettings + switch inAppSettings.totalUnreadCountDisplayStyle { + case .raw: + return (totalUnreadState.absoluteCounters.chatCount, .raw) + case .filtered: + return (totalUnreadState.filteredCounters.chatCount, .filtered) + } +} + +public func renderedTotalUnreadCount(postbox: Postbox) -> Signal<(Int32, RenderedTotalUnreadCountType), NoError> { + let unreadCountsKey = PostboxViewKey.unreadCounts(items: [UnreadMessageCountsItem.total(.raw, .chats)]) let inAppSettingsKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings])) return postbox.combinedView(keys: [unreadCountsKey, inAppSettingsKey]) - |> map { view -> Int32 in + |> map { view -> (Int32, RenderedTotalUnreadCountType) in var value: Int32 = 0 var style: TotalUnreadCountDisplayStyle = .filtered if let preferences = view.views[inAppSettingsKey] as? PreferencesView, let inAppSettings = preferences.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { style = inAppSettings.totalUnreadCountDisplayStyle } + let type: RenderedTotalUnreadCountType + switch style { + case .raw: + type = .raw + case .filtered: + type = .filtered + } if let unreadCounts = view.views[unreadCountsKey] as? UnreadMessageCountsView { switch style { case .raw: - value = unreadCounts.count(for: .total(.raw)) ?? 0 + value = unreadCounts.count(for: .total(.raw, .chats)) ?? 0 case .filtered: - value = unreadCounts.count(for: .total(.filtered)) ?? 0 + value = unreadCounts.count(for: .total(.filtered, .chats)) ?? 0 } } - return value + return (value, type) } - |> distinctUntilChanged + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) } diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index f9f7cd65d9..104a162e27 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -57,7 +57,15 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { if self.timeoutNode == nil { let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.timeoutNode = timeoutNode - timeoutNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: beginTime, timeout: timeout), completion: {}) + var iconImage: UIImage? + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: .white) { + let factor: CGFloat = 0.4 + iconImage = generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }) + } + timeoutNode.transitionToState(.secretTimeout(color: .white, icon: iconImage, beginTime: beginTime, timeout: timeout), completion: {}) self.addSubnode(timeoutNode) if let (layout, navigationHeight) = self.validLayout { diff --git a/TelegramUI/SecureIdAuthAcceptNode.swift b/TelegramUI/SecureIdAuthAcceptNode.swift index d2354ee802..e5f66eb662 100644 --- a/TelegramUI/SecureIdAuthAcceptNode.swift +++ b/TelegramUI/SecureIdAuthAcceptNode.swift @@ -25,7 +25,7 @@ final class SecureIdAuthAcceptNode: ASDisplayNode { self.buttonBackgroundNode.isLayerBacked = true self.buttonBackgroundNode.displayWithoutProcessing = true self.buttonBackgroundNode.displaysAsynchronously = false - self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: 10.0, color: theme.list.itemCheckColors.fillColor) + self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: 24.0, color: theme.list.itemCheckColors.fillColor) self.buttonNode = HighlightTrackingButtonNode() @@ -66,8 +66,8 @@ final class SecureIdAuthAcceptNode: ASDisplayNode { func updateLayout(width: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - let baseHeight: CGFloat = 80.0 - let buttonSize = CGSize(width: width - 16.0 * 2.0, height: 50.0) + let baseHeight: CGFloat = 78.0 + let buttonSize = CGSize(width: width - 16.0 * 2.0, height: 48.0) let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((baseHeight - buttonSize.height) / 2.0)), size: buttonSize) transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame) transition.updateFrame(node: self.buttonNode, frame: buttonFrame) diff --git a/TelegramUI/SecureIdAuthController.swift b/TelegramUI/SecureIdAuthController.swift index eaf2f57b96..f10dc004bf 100644 --- a/TelegramUI/SecureIdAuthController.swift +++ b/TelegramUI/SecureIdAuthController.swift @@ -12,17 +12,24 @@ final class SecureIdAuthControllerInteraction { let grant: () -> Void let openUrl: (String) -> Void let openMention: (TelegramPeerMention) -> Void + let deleteAll: () -> Void - fileprivate init(updateState: @escaping ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void, present: @escaping (ViewController, Any?) -> Void, checkPassword: @escaping (String) -> Void, grant: @escaping () -> Void, openUrl: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void) { + fileprivate init(updateState: @escaping ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void, present: @escaping (ViewController, Any?) -> Void, checkPassword: @escaping (String) -> Void, grant: @escaping () -> Void, openUrl: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void, deleteAll: @escaping () -> Void) { self.updateState = updateState self.present = present self.checkPassword = checkPassword self.grant = grant self.openUrl = openUrl self.openMention = openMention + self.deleteAll = deleteAll } } +enum SecureIdAuthControllerMode { + case form(peerId: PeerId, scope: String, publicKey: String, opaquePayload: Data) + case list +} + final class SecureIdAuthController: ViewController { private var controllerNode: SecureIdAuthControllerNode { return self.displayNode as! SecureIdAuthControllerNode @@ -30,29 +37,29 @@ final class SecureIdAuthController: ViewController { private let account: Account private var presentationData: PresentationData - private let scope: String - private let publicKey: String - private let opaquePayload: Data - private let parsedErrors: [SecureIdErrorKey: [String]] + private let mode: SecureIdAuthControllerMode private var didPlayPresentationAnimation = false private let challengeDisposable = MetaDisposable() private var formDisposable: Disposable? + private let deleteDisposable = MetaDisposable() private var state: SecureIdAuthControllerState private let hapticFeedback = HapticFeedback() - init(account: Account, peerId: PeerId, scope: String, publicKey: String, opaquePayload: Data, errors: String) { + init(account: Account, mode: SecureIdAuthControllerMode) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.scope = scope - self.publicKey = publicKey - self.opaquePayload = opaquePayload - self.parsedErrors = parseSecureIdErrors(errors) + self.mode = mode - self.state = SecureIdAuthControllerState(encryptedFormData: nil, formData: nil, verificationState: nil) + switch mode { + case .form: + self.state = .form(SecureIdAuthControllerFormState(encryptedFormData: nil, formData: nil, verificationState: nil)) + case .list: + self.state = .list(SecureIdAuthControllerListState(verificationState: nil, encryptedValues: nil, values: nil)) + } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -76,31 +83,57 @@ final class SecureIdAuthController: ViewController { } })) - self.formDisposable = (requestSecureIdForm(postbox: account.postbox, network: account.network, peerId: peerId, scope: scope, publicKey: publicKey) - |> mapToSignal { form -> Signal in - return account.postbox.modify { modifier -> Signal in - guard let accountPeer = modifier.getPeer(account.peerId), let servicePeer = modifier.getPeer(form.peerId) else { - return .fail(.generic) + switch self.mode { + case let .form(peerId, scope, publicKey, _): + self.formDisposable = (requestSecureIdForm(postbox: account.postbox, network: account.network, peerId: peerId, scope: scope, publicKey: publicKey) + |> mapToSignal { form -> Signal in + return account.postbox.transaction { transaction -> Signal in + guard let accountPeer = transaction.getPeer(account.peerId), let servicePeer = transaction.getPeer(form.peerId) else { + return .fail(.generic) + } + return .single(SecureIdEncryptedFormData(form: form, accountPeer: accountPeer, servicePeer: servicePeer)) + } + |> mapError { _ in return RequestSecureIdFormError.generic } + |> switchToLatest } - return .single(SecureIdEncryptedFormData(form: form, accountPeer: accountPeer, servicePeer: servicePeer)) - } - |> mapError { _ in return RequestSecureIdFormError.generic } - |> switchToLatest + |> deliverOnMainQueue).start(next: { [weak self] formData in + if let strongSelf = self { + strongSelf.updateState { state in + var state = state + switch state { + case var .form(form): + form.encryptedFormData = formData + state = .form(form) + case .list: + break + } + return state + } + } + }, error: { [weak self] _ in + if let strongSelf = self { + let errorText = strongSelf.presentationData.strings.Login_UnknownError + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + case .list: + self.formDisposable = (getAllSecureIdValues(network: self.account.network) + |> deliverOnMainQueue).start(next: { [weak self] values in + if let strongSelf = self { + strongSelf.updateState { state in + var state = state + switch state { + case .form: + break + case var .list(list): + list.encryptedValues = values + return .list(list) + } + return state + } + } + }) } - |> deliverOnMainQueue).start(next: { [weak self] formData in - if let strongSelf = self { - strongSelf.updateState { state in - var state = state - state.encryptedFormData = formData - return state - } - } - }, error: { [weak self] _ in - if let strongSelf = self { - let errorText = strongSelf.presentationData.strings.Login_UnknownError - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }) } required init(coder aDecoder: NSCoder) { @@ -110,6 +143,7 @@ final class SecureIdAuthController: ViewController { deinit { self.challengeDisposable.dispose() self.formDisposable?.dispose() + self.deleteDisposable.dispose() } override func viewDidAppear(_ animated: Bool) { @@ -153,7 +187,14 @@ final class SecureIdAuthController: ViewController { strongSelf.updateState { state in var state = state state.verificationState = .verified(context) - state.formData = state.encryptedFormData.flatMap({ decryptedSecureIdForm(context: context, form: $0.form) }) + switch state { + case var .form(form): + form.formData = form.encryptedFormData.flatMap({ decryptedSecureIdForm(context: context, form: $0.form) }) + state = .form(form) + case var .list(list): + list.values = list.encryptedValues.flatMap({ decryptedAllSecureIdValues(context: context, encryptedValues: $0) }) + state = .list(list) + } return state } } @@ -207,9 +248,30 @@ final class SecureIdAuthController: ViewController { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } }) + }, deleteAll: { [weak self] in + guard let strongSelf = self, case let .list(list) = strongSelf.state, let values = list.values else { + return + } + + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: strongSelf.presentationData.theme)) + strongSelf.navigationItem.rightBarButtonItem = item + strongSelf.deleteDisposable.set((deleteSecureIdValues(network: strongSelf.account.network, keys: Set(values.map({ $0.value.key }))) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + strongSelf.navigationItem.rightBarButtonItem = nil + strongSelf.updateState { state in + if case var .list(list) = state { + list.values = [] + return .list(list) + } + return state + } + })) }) - self.displayNode = SecureIdAuthControllerNode(account: self.account, presentationData: presentationData, errors: self.parsedErrors, requestLayout: { [weak self] transition in + self.displayNode = SecureIdAuthControllerNode(account: self.account, presentationData: presentationData, requestLayout: { [weak self] transition in self?.requestLayout(transition: transition) }, interaction: interaction) self.controllerNode.updateState(self.state, transition: .immediate) @@ -254,11 +316,16 @@ final class SecureIdAuthController: ViewController { } @objc private func grantAccess() { - if let encryptedFormData = self.state.encryptedFormData, let formData = self.state.formData { - let _ = (grantSecureIdAccess(network: self.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: self.publicKey, scope: self.scope, opaquePayload: self.opaquePayload, values: formData.values) - |> deliverOnMainQueue).start(completed: { [weak self] in - self?.dismiss() - }) + switch self.state { + case let .form(form): + if case let .form(reqForm) = self.mode, let encryptedFormData = form.encryptedFormData, let formData = form.formData { + let _ = (grantSecureIdAccess(network: self.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: reqForm.publicKey, scope: reqForm.scope, opaquePayload: reqForm.opaquePayload, values: formData.values) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.dismiss() + }) + } + case .list: + break } } } diff --git a/TelegramUI/SecureIdAuthControllerNode.swift b/TelegramUI/SecureIdAuthControllerNode.swift index bb2defe562..4787f11140 100644 --- a/TelegramUI/SecureIdAuthControllerNode.swift +++ b/TelegramUI/SecureIdAuthControllerNode.swift @@ -10,8 +10,6 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { private let requestLayout: (ContainedViewLayoutTransition) -> Void private let interaction: SecureIdAuthControllerInteraction - private var errors: [SecureIdErrorKey: [String]] - private var validLayout: (ContainerViewLayout, CGFloat)? private let scrollNode: ASScrollNode @@ -25,12 +23,11 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { private var state: SecureIdAuthControllerState? - init(account: Account, presentationData: PresentationData, errors: [SecureIdErrorKey: [String]], requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) { + init(account: Account, presentationData: PresentationData, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) { self.account = account self.presentationData = presentationData self.requestLayout = requestLayout self.interaction = interaction - self.errors = errors self.scrollNode = ASScrollNode() self.headerNode = SecureIdAuthHeaderNode(account: account, theme: presentationData.theme, strings: presentationData.strings) @@ -65,7 +62,12 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { insets.bottom = max(insets.bottom, layout.safeInsets.bottom) let headerNodeTransition: ContainedViewLayoutTransition = headerNode.bounds.isEmpty ? .immediate : transition - let headerHeight = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition) + let headerHeight: CGFloat + if self.headerNode.alpha.isZero { + headerHeight = 0.0 + } else { + headerHeight = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition) + } let acceptHeight = self.acceptNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition) @@ -76,13 +78,18 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { footerHeight += acceptHeight contentSpacing = 25.0 } else { - contentSpacing = 56.0 + if self.contentNode is SecureIdAuthListContentNode { + contentSpacing = 16.0 + } else { + contentSpacing = 56.0 + } } insets.bottom += footerHeight let wrappingContentRect = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - insets.bottom - navigationBarHeight)) let contentRect = CGRect(origin: CGPoint(), size: wrappingContentRect.size) + let overscrollY = self.scrollNode.view.bounds.minY transition.updateFrame(node: self.scrollNode, frame: wrappingContentRect) if let contentNode = self.contentNode { @@ -92,10 +99,19 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { let boundingHeight = headerHeight + contentLayout.height + contentSpacing - var boundingRect = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY + floor((contentRect.height - boundingHeight) / 2.0)), size: CGSize(width: layout.size.width, height: boundingHeight)) + var boundingRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: boundingHeight)) + if contentNode is SecureIdAuthListContentNode { + boundingRect.origin.y = contentRect.minY + } else { + boundingRect.origin.y = contentRect.minY + floor((contentRect.height - boundingHeight) / 2.0) + } boundingRect.origin.y = max(boundingRect.origin.y, 14.0) - headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY), size: CGSize(width: boundingRect.width, height: headerHeight))) + if self.headerNode.alpha.isZero { + headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: -boundingRect.width, y: self.headerNode.frame.minY), size: CGSize(width: boundingRect.width, height: headerHeight))) + } else { + headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY), size: CGSize(width: boundingRect.width, height: headerHeight))) + } contentNodeTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY + headerHeight + contentSpacing), size: CGSize(width: boundingRect.width, height: contentLayout.height))) @@ -109,7 +125,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } } - self.scrollNode.view.contentSize = CGSize(width: boundingRect.width, height: boundingRect.maxY) + self.scrollNode.view.contentSize = CGSize(width: boundingRect.width, height: 14.0 + boundingRect.height + 16.0) } if let dismissedContentNode = self.dismissedContentNode { @@ -142,72 +158,133 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { func updateState(_ state: SecureIdAuthControllerState, transition: ContainedViewLayoutTransition) { self.state = state - if let encryptedFormData = state.encryptedFormData, let verificationState = state.verificationState { - if self.headerNode.supernode == nil { - self.scrollNode.addSubnode(self.headerNode) - self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - self.headerNode.updateState(formData: encryptedFormData, verificationState: verificationState) - - var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? - - switch verificationState { - case let .passwordChallenge(hint, challengeState): - if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { - current.updateIsChecking(challengeState == .checking) - contentNode = current - } else { - let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in - if let strongSelf = self { - strongSelf.interaction.checkPassword(password) - } - }, passwordHelp: { [weak self] in - if let strongSelf = self { - - } - }) - current.updateIsChecking(challengeState == .checking) - contentNode = current + switch state { + case let .form(form): + if let encryptedFormData = form.encryptedFormData, let verificationState = form.verificationState { + if self.headerNode.supernode == nil { + self.scrollNode.addSubnode(self.headerNode) + self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - case .noChallenge: - contentNode = nil - case .verified: - if let encryptedFormData = state.encryptedFormData, let formData = state.formData { - if let current = self.contentNode as? SecureIdAuthFormContentNode { - current.updateValues(formData.values, errors: self.errors) - contentNode = current - } else { - let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, errors: self.errors, openField: { [weak self] field in - if let strongSelf = self { - switch field { - case .identity, .address: - strongSelf.presentDocumentSelection(field: field) - case .phone: - strongSelf.presentPlaintextSelection(type: .phone) - case .email: - strongSelf.presentPlaintextSelection(type: .email) + self.headerNode.updateState(formData: encryptedFormData, verificationState: verificationState) + + var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? + + switch verificationState { + case let .passwordChallenge(hint, challengeState): + if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { + current.updateIsChecking(challengeState == .checking) + contentNode = current + } else { + let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in + if let strongSelf = self { + strongSelf.interaction.checkPassword(password) } + }, passwordHelp: { [weak self] in + if let strongSelf = self { + + } + }) + current.updateIsChecking(challengeState == .checking) + contentNode = current + } + case .noChallenge: + contentNode = nil + case .verified: + if let encryptedFormData = form.encryptedFormData, let formData = form.formData { + if let current = self.contentNode as? SecureIdAuthFormContentNode { + current.updateValues(formData.values) + contentNode = current + } else { + let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, openField: { [weak self] field in + if let strongSelf = self { + switch field { + case .identity, .address: + strongSelf.presentDocumentSelection(field: field) + case .phone: + strongSelf.presentPlaintextSelection(type: .phone) + case .email: + strongSelf.presentPlaintextSelection(type: .email) + } + } + }, openURL: { [weak self] url in + self?.interaction.openUrl(url) + }, openMention: { [weak self] mention in + self?.interaction.openMention(mention) + }) + contentNode = current } - }, openURL: { [weak self] url in - self?.interaction.openUrl(url) - }, openMention: { [weak self] mention in - self?.interaction.openMention(mention) - }) - contentNode = current + } + } + + if case .verified = verificationState { + if self.acceptNode.supernode == nil { + self.addSubnode(self.acceptNode) + self.acceptNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.acceptNode.bounds.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } - } - - if case .verified = verificationState { - if self.acceptNode.supernode == nil { - self.addSubnode(self.acceptNode) - self.acceptNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.acceptNode.bounds.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + if self.contentNode !== contentNode { + self.transitionToContentNode(contentNode, transition: transition) + } + } + case let .list(list): + if let _ = list.encryptedValues, let verificationState = list.verificationState { + if case .verified = verificationState { + if !self.headerNode.alpha.isZero { + self.headerNode.alpha = 0.0 + self.headerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } else { + if self.headerNode.supernode == nil { + self.scrollNode.addSubnode(self.headerNode) + self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + self.headerNode.updateState(formData: nil, verificationState: verificationState) + } + + var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? + + switch verificationState { + case let .passwordChallenge(hint, challengeState): + if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { + current.updateIsChecking(challengeState == .checking) + contentNode = current + } else { + let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in + if let strongSelf = self { + strongSelf.interaction.checkPassword(password) + } + }, passwordHelp: { [weak self] in + if let strongSelf = self { + + } + }) + current.updateIsChecking(challengeState == .checking) + contentNode = current + } + case .noChallenge: + contentNode = nil + case .verified: + if let _ = list.encryptedValues, let values = list.values { + if let current = self.contentNode as? SecureIdAuthListContentNode { + current.updateValues(values) + contentNode = current + } else { + let current = SecureIdAuthListContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, openField: { [weak self] field in + self?.openListField(field) + }, deleteAll: { [weak self] in + self?.deleteAllValues() + }) + current.updateValues(values) + contentNode = current + } + } + } + + if self.contentNode !== contentNode { + self.transitionToContentNode(contentNode, transition: transition) + } } - } - - if self.contentNode !== contentNode { - self.transitionToContentNode(contentNode, transition: transition) - } } } @@ -227,73 +304,75 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } private func presentDocumentSelection(field: SecureIdParsedRequestedFormField) { - guard let state = self.state, let verificationState = state.verificationState, case let .verified(context) = verificationState, let formData = state.formData else { + guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(context) = verificationState, let formData = form.formData else { return } let updatedValue: ([SecureIdValueWithContext]) -> Void = { [weak self] updatedValues in if let strongSelf = self { strongSelf.interaction.updateState { state in - if let formData = state.formData { - var values = formData.values.filter { value in - switch field { - case let .identity(personalDetails, document, _): - if personalDetails { - if case .personalDetails = value.value.key { - return false - } - } - switch value.value.key { - case .passport: - if document.contains(.passport) { - return false + switch state { + case let .form(form): + if let formData = form.formData { + var values = formData.values.filter { value in + switch field { + case let .identity(personalDetails, document, _): + if personalDetails { + if case .personalDetails = value.value.key { + return false + } } - case .driversLicense: - if document.contains(.driversLicense) { - return false + switch value.value.key { + case .passport: + if document.contains(.passport) { + return false + } + case .driversLicense: + if document.contains(.driversLicense) { + return false + } + case .idCard: + if document.contains(.idCard) { + return false + } + default: + break } - case .idCard: - if document.contains(.idCard) { - return false + case let .address(addressDetails, document): + if addressDetails { + if case .address = value.value.key { + return false + } } - default: + switch value.value.key { + case .bankStatement: + if document.contains(.bankStatement) { + return false + } + case .utilityBill: + if document.contains(.utilityBill) { + return false + } + case .rentalAgreement: + if document.contains(.rentalAgreement) { + return false + } + default: + break + } + case .phone: + break + case .email: break } - case let .address(addressDetails, document): - if addressDetails { - if case .address = value.value.key { - return false - } - } - switch value.value.key { - case .bankStatement: - if document.contains(.bankStatement) { - return false - } - case .utilityBill: - if document.contains(.utilityBill) { - return false - } - case .rentalAgreement: - if document.contains(.rentalAgreement) { - return false - } - default: - break - } - case .phone: - break - case .email: - break + return true + } + values.append(contentsOf: updatedValues) + + return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState)) } - return true - } - values.append(contentsOf: updatedValues) - - //strongSelf.errors = filterSecureIdErrors(errors: strongSelf.errors, afterSaving: updatedValues) - strongSelf.errors = [:] - - return SecureIdAuthControllerState(encryptedFormData: state.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: state.verificationState) + case let .list(list): + break } return state } @@ -316,6 +395,11 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { hasValueType = .passport break loop } + case .internalPassport: + if findValue(formData.values, key: .internalPassport) != nil { + hasValueType = .internalPassport + break loop + } case .driversLicense: if findValue(formData.values, key: .driversLicense) != nil { hasValueType = .driversLicense @@ -329,13 +413,23 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } } if hasValueType != nil || hasPersonalDetails { - self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .identity(details: personalDetails, document: hasValueType, selfie: selfie), values: formData.values, errors: self.errors, updatedValues: updatedValue), nil) + self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .identity(details: personalDetails, document: hasValueType, selfie: selfie), values: formData.values, updatedValues: updatedValue), nil) return } case let .address(addressDetails, document): var hasValueType: SecureIdRequestedAddressDocument? loop: for documentType in document { switch documentType { + case .passportRegistration: + if findValue(formData.values, key: .passportRegistration) != nil { + hasValueType = .passportRegistration + break loop + } + case .temporaryRegistration: + if findValue(formData.values, key: .temporaryRegistration) != nil { + hasValueType = .temporaryRegistration + break loop + } case .bankStatement: if findValue(formData.values, key: .bankStatement) != nil { hasValueType = .bankStatement @@ -354,7 +448,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } } if let hasValueType = hasValueType { - self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .address(details: addressDetails, document: hasValueType), values: formData.values, errors: self.errors, updatedValues: updatedValue), nil) + self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .address(details: addressDetails, document: hasValueType), values: formData.values, updatedValues: updatedValue), nil) return } default: @@ -362,24 +456,24 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } let controller = SecureIdDocumentTypeSelectionController(theme: self.presentationData.theme, strings: self.presentationData.strings, field: field, currentValues: formData.values, completion: { [weak self] requestedData in - guard let strongSelf = self, let state = strongSelf.state, let verificationState = state.verificationState, case let .verified(context) = verificationState, let formData = state.formData else { + guard let strongSelf = self, let state = strongSelf.state, let verificationState = state.verificationState, case let .verified(context) = verificationState, let formData = form.formData else { return } - strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: requestedData, values: formData.values, errors: strongSelf.errors, updatedValues: updatedValue), nil) + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: requestedData, values: formData.values, updatedValues: updatedValue), nil) }) self.interaction.present(controller, nil) } private func presentPlaintextSelection(type: SecureIdPlaintextFormType) { - guard let state = self.state, let verificationState = state.verificationState, case let .verified(context) = verificationState, let formData = state.formData else { + guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(context) = verificationState, let formData = form.formData else { return } var immediatelyAvailableValue: SecureIdValue? switch type { case .phone: - if let peer = state.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { + if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } default: @@ -388,7 +482,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { [weak self] valueWithContext in if let strongSelf = self { strongSelf.interaction.updateState { state in - if let formData = state.formData { + if case let .form(form) = state, let formData = form.formData { var values = formData.values switch type { case .phone: @@ -403,11 +497,146 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { if let valueWithContext = valueWithContext { values.append(valueWithContext) } - return SecureIdAuthControllerState(encryptedFormData: state.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: state.verificationState) + return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState)) } return state } } }), nil) } + + private func openListField(_ field: SecureIdAuthListContentField) { + guard let state = self.state, case let .list(list) = state, let verificationState = list.verificationState, case let .verified(context) = verificationState else { + return + } + guard let values = list.values else { + return + } + + let updatedValues: (SecureIdValueKey) -> ([SecureIdValueWithContext]) -> Void = { valueKey in + return { [weak self] updatedValues in + guard let strongSelf = self else { + return + } + strongSelf.interaction.updateState { state in + guard case var .list(list) = state, var values = list.values else { + return state + } + + values = values.filter({ value in + return value.value.key != valueKey + }) + + values.append(contentsOf: updatedValues) + + list.values = values + return .list(list) + } + } + } + + let openAction: (SecureIdValueKey) -> Void = { [weak self] field in + guard let strongSelf = self else { + return + } + switch field { + case .personalDetails: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: true, document: nil, selfie: false), values: values, updatedValues: updatedValues(field)), nil) + case .passport: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .passport, selfie: true), values: values, updatedValues: updatedValues(field)), nil) + case .internalPassport: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .internalPassport, selfie: true), values: values, updatedValues: updatedValues(field)), nil) + case .driversLicense: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .driversLicense, selfie: true), values: values, updatedValues: updatedValues(field)), nil) + case .idCard: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .idCard, selfie: true), values: values, updatedValues: updatedValues(field)), nil) + case .passportRegistration: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .passportRegistration), values: values, updatedValues: updatedValues(field)), nil) + case .temporaryRegistration: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .temporaryRegistration), values: values, updatedValues: updatedValues(field)), nil) + case .address: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: true, document: nil), values: values, updatedValues: updatedValues(field)), nil) + case .utilityBill: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .utilityBill), values: values, updatedValues: updatedValues(field)), nil) + case .bankStatement: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .bankStatement), values: values, updatedValues: updatedValues(field)), nil) + case .rentalAgreement: + strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .rentalAgreement), values: values, updatedValues: updatedValues(field)), nil) + case .phone: + break + case .email: + break + } + } + + switch field { + case .identity, .address: + let keys: [(SecureIdValueKey, String, String)] + if case .identity = field { + keys = [ + (.personalDetails, "Add Personal Details", "Edit Personal Details"), + (.passport, "Add Passport", "Edit Passport"), + (.idCard, "Add Identity Card", "Edit Identity Card"), + (.driversLicense, "Add Driver's License", "Edit Driver's License"), + (.internalPassport, "Add Internal Passport", "Edit Internal Passport"), + ] + } else { + keys = [ + (.address, "Add Residential Address", "Edit Residential Address"), + (.utilityBill, "Add Utility Bill", "Edit Utility Bill"), + (.bankStatement, "Add Bank Statement", "Edit Bank Statement"), + (.rentalAgreement, "Add Rental Agreement", "Edit Rental Agreement"), + (.passportRegistration, "Add Passport Registration", "Edit Passport Registration"), + (.temporaryRegistration, "Add Temporary Registration", "Edit Temporary Registration") + ] + } + + let controller = ActionSheetController(presentationTheme: self.presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + for (key, add, edit) in keys { + items.append(ActionSheetButtonItem(title: findValue(values, key: key) != nil ? edit : add, action: { + dismissAction() + openAction(key) + })) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.view.endEditing(true) + self.interaction.present(controller, nil) + case .phone: + var immediatelyAvailableValue: SecureIdValue? + self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in + updatedValues(.phone)(value.flatMap({ [$0] }) ?? []) + }), nil) + case .email: + self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: .email, immediatelyAvailableValue: nil, updatedValue: { value in + updatedValues(.email)(value.flatMap({ [$0] }) ?? []) + }), nil) + } + } + + private func deleteAllValues() { + let controller = ActionSheetController(presentationTheme: self.presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let items: [ActionSheetItem] = [ + ActionSheetTextItem(title: "Are you sure you want to delete your Telegram Passport? All details will be lost."), + ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, enabled: true, action: { [weak self] in + dismissAction() + self?.interaction.deleteAll() + }) + ] + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.view.endEditing(true) + self.interaction.present(controller, nil) + } } diff --git a/TelegramUI/SecureIdAuthControllerState.swift b/TelegramUI/SecureIdAuthControllerState.swift index dee3223044..8c6539547c 100644 --- a/TelegramUI/SecureIdAuthControllerState.swift +++ b/TelegramUI/SecureIdAuthControllerState.swift @@ -43,12 +43,12 @@ enum SecureIdAuthControllerVerificationState: Equatable { } } -struct SecureIdAuthControllerState: Equatable { +struct SecureIdAuthControllerFormState: Equatable { var encryptedFormData: SecureIdEncryptedFormData? var formData: SecureIdForm? var verificationState: SecureIdAuthControllerVerificationState? - static func ==(lhs: SecureIdAuthControllerState, rhs: SecureIdAuthControllerState) -> Bool { + static func ==(lhs: SecureIdAuthControllerFormState, rhs: SecureIdAuthControllerFormState) -> Bool { if (lhs.formData != nil) != (rhs.formData != nil) { return false } @@ -72,3 +72,47 @@ struct SecureIdAuthControllerState: Equatable { return true } } + +struct SecureIdAuthControllerListState: Equatable { + var verificationState: SecureIdAuthControllerVerificationState? + var encryptedValues: EncryptedAllSecureIdValues? + var values: [SecureIdValueWithContext]? + + static func ==(lhs: SecureIdAuthControllerListState, rhs: SecureIdAuthControllerListState) -> Bool { + if lhs.verificationState != rhs.verificationState { + return false + } + if (lhs.encryptedValues != nil) != (rhs.encryptedValues != nil) { + return false + } + if lhs.values != rhs.values { + return false + } + return true + } +} + +enum SecureIdAuthControllerState: Equatable { + case form(SecureIdAuthControllerFormState) + case list(SecureIdAuthControllerListState) + + var verificationState: SecureIdAuthControllerVerificationState? { + get { + switch self { + case let .form(form): + return form.verificationState + case let .list(list): + return list.verificationState + } + } set(value) { + switch self { + case var .form(form): + form.verificationState = value + self = .form(form) + case var .list(list): + list.verificationState = value + self = .list(list) + } + } + } +} diff --git a/TelegramUI/SecureIdAuthFormContentNode.swift b/TelegramUI/SecureIdAuthFormContentNode.swift index e1e9345e41..fb0f2cb3b4 100644 --- a/TelegramUI/SecureIdAuthFormContentNode.swift +++ b/TelegramUI/SecureIdAuthFormContentNode.swift @@ -15,7 +15,7 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, private var validLayout: CGFloat? - init(theme: PresentationTheme, strings: PresentationStrings, peer: Peer, privacyPolicyUrl: String?, form: SecureIdForm, errors: [SecureIdErrorKey: [String]], openField: @escaping (SecureIdParsedRequestedFormField) -> Void, openURL: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, peer: Peer, privacyPolicyUrl: String?, form: SecureIdForm, openField: @escaping (SecureIdParsedRequestedFormField) -> Void, openURL: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void) { self.fieldBackgroundNode = ASDisplayNode() self.fieldBackgroundNode.isLayerBacked = true self.fieldBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor @@ -23,7 +23,7 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, var fieldNodes: [SecureIdAuthFormFieldNode] = [] for field in parseRequestedFormFields(form.requestedFields) { - fieldNodes.append(SecureIdAuthFormFieldNode(theme: theme, strings: strings, field: field, values: form.values, errors: errors, selected: { + fieldNodes.append(SecureIdAuthFormFieldNode(theme: theme, strings: strings, field: field, values: form.values, selected: { openField(field) })) } @@ -42,7 +42,7 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, let textData = strings.SecureId_FormPolicy(strings.SecureId_FormPolicyLink(peer.displayTitle).0, "@" + (peer.addressName ?? "")) text.append(NSAttributedString(string: textData.0, font: Font.regular(14.0), textColor: theme.list.freeTextColor)) for (index, range) in textData.1 { - if index == 0 { + if index == 2 { text.addAttribute(.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue, range: range) text.addAttribute(.foregroundColor, value: theme.list.itemAccentColor, range: range) if let privacyPolicyUrl = privacyPolicyUrl { @@ -78,9 +78,9 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, self.fieldNodes.forEach(self.addSubnode) } - func updateValues(_ values: [SecureIdValueWithContext], errors: [SecureIdErrorKey: [String]]) { + func updateValues(_ values: [SecureIdValueWithContext]) { for fieldNode in self.fieldNodes { - fieldNode.updateValues(values, errors: errors) + fieldNode.updateValues(values) } } diff --git a/TelegramUI/SecureIdAuthFormFieldNode.swift b/TelegramUI/SecureIdAuthFormFieldNode.swift index 8db68359af..88cf52da4e 100644 --- a/TelegramUI/SecureIdAuthFormFieldNode.swift +++ b/TelegramUI/SecureIdAuthFormFieldNode.swift @@ -5,6 +5,7 @@ import TelegramCore enum SecureIdRequestedIdentityDocument: Int32 { case passport + case internalPassport case driversLicense case idCard @@ -12,6 +13,8 @@ enum SecureIdRequestedIdentityDocument: Int32 { switch self { case .passport: return .passport + case .internalPassport: + return .internalPassport case .driversLicense: return .driversLicense case .idCard: @@ -21,12 +24,18 @@ enum SecureIdRequestedIdentityDocument: Int32 { } enum SecureIdRequestedAddressDocument: Int32 { + case passportRegistration + case temporaryRegistration case bankStatement case utilityBill case rentalAgreement var valueKey: SecureIdValueKey { switch self { + case .passportRegistration: + return .passportRegistration + case .temporaryRegistration: + return .temporaryRegistration case .bankStatement: return .bankStatement case .utilityBill: @@ -57,6 +66,9 @@ func parseRequestedFormFields(_ types: [SecureIdRequestedFormField]) -> [SecureI case let .passport(selfie): identity.1.insert(.passport) identity.2 = identity.2 || selfie + case let .internalPassport(selfie): + identity.1.insert(.internalPassport) + identity.2 = identity.2 || selfie case let .driversLicense(selfie): identity.1.insert(.driversLicense) identity.2 = identity.2 || selfie @@ -65,6 +77,10 @@ func parseRequestedFormFields(_ types: [SecureIdRequestedFormField]) -> [SecureI identity.2 = identity.2 || selfie case .address: address.0 = true + case .passportRegistration: + address.1.insert(.passportRegistration) + case .temporaryRegistration: + address.1.insert(.temporaryRegistration) case .bankStatement: address.1.insert(.bankStatement) case .utilityBill: @@ -140,6 +156,8 @@ private func fieldTitleAndText(field: SecureIdParsedRequestedFormField, strings: switch documentType { case .passport: key = .passport + case .internalPassport: + key = .internalPassport case .driversLicense: key = .driversLicense case .idCard: @@ -216,7 +234,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings - init(theme: PresentationTheme, strings: PresentationStrings, field: SecureIdParsedRequestedFormField, values: [SecureIdValueWithContext], errors: [SecureIdErrorKey: [String]], selected: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, field: SecureIdParsedRequestedFormField, values: [SecureIdValueWithContext], selected: @escaping () -> Void) { self.field = field self.theme = theme self.strings = strings @@ -270,7 +288,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode { self.addSubnode(self.checkNode) self.addSubnode(self.buttonNode) - self.updateValues(values, errors: errors) + self.updateValues(values) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -287,10 +305,10 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } - func updateValues(_ values: [SecureIdValueWithContext], errors: [SecureIdErrorKey: [String]]) { + func updateValues(_ values: [SecureIdValueWithContext]) { var (title, text) = fieldTitleAndText(field: self.field, strings: self.strings, values: values) var textColor = self.theme.list.itemSecondaryTextColor - switch self.field { + /*switch self.field { case .identity: if let error = errors[.personalDetails]?.first { text = error @@ -298,7 +316,7 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode { } default: break - } + }*/ self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.theme.list.itemPrimaryTextColor) self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: textColor) diff --git a/TelegramUI/SecureIdAuthHeaderNode.swift b/TelegramUI/SecureIdAuthHeaderNode.swift index 2dcfb218ff..4c18a4130f 100644 --- a/TelegramUI/SecureIdAuthHeaderNode.swift +++ b/TelegramUI/SecureIdAuthHeaderNode.swift @@ -6,8 +6,8 @@ import TelegramCore import SwiftSignalKit private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 26.0)! -private let titleFont: UIFont = Font.semibold(16.0) -private let textFont: UIFont = Font.regular(16.0) +private let titleFont: UIFont = Font.semibold(14.0) +private let textFont: UIFont = Font.regular(14.0) final class SecureIdAuthHeaderNode: ASDisplayNode { private let account: Account @@ -16,6 +16,7 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { private let serviceAvatarNode: AvatarNode private let titleNode: ImmediateTextNode + private let iconNode: ASImageNode private var verificationState: SecureIdAuthControllerVerificationState? @@ -29,41 +30,65 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { self.titleNode.maximumNumberOfLines = 0 self.titleNode.textAlignment = .center + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/ViewPassportIcon"), color: theme.list.freeMonoIcon) + super.init() self.addSubnode(self.serviceAvatarNode) self.addSubnode(self.titleNode) + self.addSubnode(self.iconNode) } - func updateState(formData: SecureIdEncryptedFormData, verificationState: SecureIdAuthControllerVerificationState) { - self.serviceAvatarNode.setPeer(account: self.account, peer: formData.servicePeer) + func updateState(formData: SecureIdEncryptedFormData?, verificationState: SecureIdAuthControllerVerificationState) { + if let formData = formData { + self.serviceAvatarNode.setPeer(account: self.account, peer: formData.servicePeer) + let titleData = self.strings.SecureId_RequestTitle(formData.servicePeer.displayTitle) + + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: titleData.0, font: textFont, textColor: self.theme.list.freeTextColor)) + for (_, range) in titleData.1 { + titleString.addAttribute(.font, value: titleFont, range: range) + } + self.titleNode.attributedText = titleString + self.iconNode.isHidden = true + } else { + self.iconNode.isHidden = false + self.titleNode.isHidden = true + self.serviceAvatarNode.isHidden = true + } self.verificationState = verificationState - - let titleData = self.strings.SecureId_RequestTitle(formData.servicePeer.displayTitle) - - let titleString = NSMutableAttributedString() - titleString.append(NSAttributedString(string: titleData.0, font: textFont, textColor: self.theme.list.freeTextColor)) - for (_, range) in titleData.1 { - titleString.addAttribute(.font, value: titleFont, range: range) - } - self.titleNode.attributedText = titleString } func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let avatarSize = CGSize(width: 70.0, height: 70.0) - - let serviceAvatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize) - transition.updateFrame(node: self.serviceAvatarNode, frame: serviceAvatarFrame) - - let avatarTitleSpacing: CGFloat = 20.0 - - let titleSize = self.titleNode.updateLayout(CGSize(width: width - 20.0, height: 1000.0)) - - let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarSize.height + avatarTitleSpacing), size: titleSize) - ContainedViewLayoutTransition.immediate.updateFrame(node: self.titleNode, frame: titleFrame) - - let resultHeight: CGFloat = avatarSize.height + avatarTitleSpacing + titleSize.height - return resultHeight + if !self.iconNode.isHidden { + guard let image = self.iconNode.image else { + return 1.0 + } + + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0), y: 0.0), size: image.size) + + let resultHeight: CGFloat = image.size.height + return resultHeight + } else { + let avatarSize = CGSize(width: 70.0, height: 70.0) + + let serviceAvatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize) + transition.updateFrame(node: self.serviceAvatarNode, frame: serviceAvatarFrame) + + let avatarTitleSpacing: CGFloat = 20.0 + + let titleSize = self.titleNode.updateLayout(CGSize(width: width - 20.0, height: 1000.0)) + + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarSize.height + avatarTitleSpacing), size: titleSize) + ContainedViewLayoutTransition.immediate.updateFrame(node: self.titleNode, frame: titleFrame) + + let resultHeight: CGFloat = avatarSize.height + avatarTitleSpacing + titleSize.height + return resultHeight + } } } diff --git a/TelegramUI/SecureIdAuthListContentNode.swift b/TelegramUI/SecureIdAuthListContentNode.swift new file mode 100644 index 0000000000..9002408eff --- /dev/null +++ b/TelegramUI/SecureIdAuthListContentNode.swift @@ -0,0 +1,118 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +final class SecureIdAuthListContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let fieldBackgroundNode: ASDisplayNode + private let fieldNodes: [SecureIdAuthListFieldNode] + private let headerNode: ImmediateTextNode + + private let deleteItem: FormControllerActionItem + private let deleteNode: FormControllerActionItemNode + + private var validLayout: CGFloat? + + init(theme: PresentationTheme, strings: PresentationStrings, openField: @escaping (SecureIdAuthListContentField) -> Void, deleteAll: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.fieldBackgroundNode = ASDisplayNode() + self.fieldBackgroundNode.isLayerBacked = true + self.fieldBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor + + var fieldNodes: [SecureIdAuthListFieldNode] = [] + fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .identity, values: [], selected: { + openField(.identity) + })) + fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .address, values: [], selected: { + openField(.address) + })) + fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .phone, values: [], selected: { + openField(.phone) + })) + fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .email, values: [], selected: { + openField(.email) + })) + + self.fieldNodes = fieldNodes + + self.headerNode = ImmediateTextNode() + self.headerNode.displaysAsynchronously = false + self.headerNode.attributedText = NSAttributedString(string: "PASSPORT INFORMATION", font: Font.regular(14.0), textColor: theme.list.sectionHeaderTextColor) + + self.deleteItem = FormControllerActionItem(type: .destructive, title: "Delete Passport", activated: { + deleteAll() + }) + self.deleteNode = self.deleteItem.node() as! FormControllerActionItemNode + + super.init() + + self.addSubnode(self.headerNode) + self.addSubnode(self.fieldBackgroundNode) + self.addSubnode(self.deleteNode) + self.fieldNodes.forEach(self.addSubnode) + } + + func updateValues(_ values: [SecureIdValueWithContext]) { + for fieldNode in self.fieldNodes { + fieldNode.updateValues(values) + } + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout { + let transition = self.validLayout == nil ? .immediate : transition + self.validLayout = width + + var contentHeight: CGFloat = 0.0 + + let headerSpacing: CGFloat = 6.0 + let headerSize = self.headerNode.updateLayout(CGSize(width: width - 14.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: headerSize)) + contentHeight += headerSize.height + headerSpacing + + let fieldsOrigin = contentHeight + for i in 0 ..< self.fieldNodes.count { + let fieldHeight = self.fieldNodes[i].updateLayout(width: width, hasPrevious: i != 0, hasNext: i != self.fieldNodes.count - 1, transition: transition) + transition.updateFrame(node: self.fieldNodes[i], frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: fieldHeight))) + contentHeight += fieldHeight + } + + let fieldsHeight = contentHeight - fieldsOrigin + + transition.updateFrame(node: self.fieldBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: fieldsOrigin), size: CGSize(width: width, height: fieldsHeight))) + + let deleteSpacing: CGFloat = 32.0 + contentHeight += deleteSpacing + + let (preLayout, apply) = self.deleteItem.update(node: self.deleteNode, theme: self.theme, strings: self.strings, width: width, previousNeighbor: .spacer, nextNeighbor: .spacer, transition: transition) + let deleteHeight = apply(FormControllerItemLayoutParams(maxAligningInset: preLayout.aligningInset)) + transition.updateFrame(node: self.deleteNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: deleteHeight))) + + contentHeight += deleteHeight + contentHeight += deleteSpacing + + return SecureIdAuthContentLayout(height: contentHeight, centerOffset: floor((contentHeight) / 2.0)) + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func didAppear() { + } + + func willDisappear() { + } +} + diff --git a/TelegramUI/SecureIdAuthListFieldNode.swift b/TelegramUI/SecureIdAuthListFieldNode.swift new file mode 100644 index 0000000000..95db1749de --- /dev/null +++ b/TelegramUI/SecureIdAuthListFieldNode.swift @@ -0,0 +1,251 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore + +private let titleFont = Font.regular(17.0) +private let textFont = Font.regular(15.0) + +private func fieldsText(_ fields: String...) -> String { + var result = "" + for field in fields { + if !field.isEmpty { + if !result.isEmpty { + result.append(", ") + } + result.append(field) + } + } + return result +} + +private func fieldsText(_ fields: [String]) -> String { + var result = "" + for field in fields { + if !field.isEmpty { + if !result.isEmpty { + result.append(", ") + } + result.append(field) + } + } + return result +} + +private func fieldTitleAndText(field: SecureIdAuthListContentField, strings: PresentationStrings, values: [SecureIdValueWithContext]) -> (String, String) { + let title: String + let placeholder: String + var text: String = "" + + switch field { + case .identity: + title = strings.SecureId_FormFieldIdentity + placeholder = strings.SecureId_FormFieldIdentityPlaceholder + + let keyList: [(SecureIdValueKey, String)] = [ + (.passport, "Passport"), + (.personalDetails, "Personal Details"), + (.internalPassport, "Internal Passport"), + (.driversLicense, "Driver's License"), + (.idCard, "ID Card") + ] + + var fields: [String] = [] + for (key, valueTitle) in keyList { + if findValue(values, key: key) != nil { + fields.append(valueTitle) + } + } + + if !fields.isEmpty { + text = fieldsText(fields) + } + case .address: + title = strings.SecureId_FormFieldAddress + placeholder = strings.SecureId_FormFieldAddressPlaceholder + + let keyList: [(SecureIdValueKey, String)] = [ + (.address, "Address"), + (.passportRegistration, "Passport Registration"), + (.temporaryRegistration, "Temporary Registration"), + (.utilityBill, "Utility Bill"), + (.bankStatement, "Bank Statement"), + (.rentalAgreement, "Rental Agreement") + ] + + var fields: [String] = [] + for (key, valueTitle) in keyList { + if findValue(values, key: key) != nil { + fields.append(valueTitle) + } + } + + if !fields.isEmpty { + text = fieldsText(fields) + } + case .phone: + title = strings.SecureId_FormFieldPhone + placeholder = strings.SecureId_FormFieldPhonePlaceholder + + if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1 { + if !text.isEmpty { + text.append(", ") + } + text = formatPhoneNumber(phoneValue.phone) + } + case .email: + title = strings.SecureId_FormFieldEmail + placeholder = strings.SecureId_FormFieldEmailPlaceholder + + if let value = findValue(values, key: .email), case let .email(emailValue) = value.1 { + if !text.isEmpty { + text.append(", ") + } + text = formatPhoneNumber(emailValue.email) + } + } + + return (title, text.isEmpty ? placeholder : text) +} + +enum SecureIdAuthListContentField { + case identity + case address + case phone + case email +} + +final class SecureIdAuthListFieldNode: ASDisplayNode { + private let selected: () -> Void + + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let disclosureNode: ASImageNode + + private let buttonNode: HighlightableButtonNode + + private var validLayout: (CGFloat, Bool, Bool)? + + private let field: SecureIdAuthListContentField + private let theme: PresentationTheme + private let strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings, field: SecureIdAuthListContentField, values: [SecureIdValueWithContext], selected: @escaping () -> Void) { + self.field = field + self.theme = theme + self.strings = strings + self.selected = selected + + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.isLayerBacked = true + self.titleNode.maximumNumberOfLines = 1 + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isLayerBacked = true + self.textNode.maximumNumberOfLines = 1 + + self.disclosureNode = ASImageNode() + self.disclosureNode.isLayerBacked = true + self.disclosureNode.displayWithoutProcessing = true + self.disclosureNode.displaysAsynchronously = false + self.disclosureNode.image = PresentationResourcesItemList.disclosureArrowImage(theme) + + self.buttonNode = HighlightableButtonNode() + + super.init() + + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.disclosureNode) + self.addSubnode(self.buttonNode) + + self.updateValues(values) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + strongSelf.view.superview?.bringSubview(toFront: strongSelf.view) + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateValues(_ values: [SecureIdValueWithContext]) { + let (title, text) = fieldTitleAndText(field: self.field, strings: self.strings, values: values) + let textColor = self.theme.list.itemSecondaryTextColor + self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.theme.list.itemPrimaryTextColor) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: textColor) + + self.disclosureNode.isHidden = false + + if let (width, hasPrevious, hasNext) = self.validLayout { + let _ = self.updateLayout(width: width, hasPrevious: hasPrevious, hasNext: hasNext, transition: .immediate) + } + } + + func updateLayout(width: CGFloat, hasPrevious: Bool, hasNext: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, hasPrevious, hasNext) + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 16.0 + let height: CGFloat = 64.0 + + let rightTextInset = rightInset + 24.0 + + let titleTextSpacing: CGFloat = 5.0 + + let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - rightTextInset, height: 100.0)) + let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightTextInset, height: 100.0)) + + let textOrigin = floor((height - titleSize.height - titleTextSpacing - textSize.height) / 2.0) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: textOrigin), size: titleSize) + self.titleNode.frame = titleFrame + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleTextSpacing), size: textSize) + self.textNode.frame = textFrame + + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateAlpha(node: self.topSeparatorNode, alpha: hasPrevious ? 0.0 : 1.0) + let bottomSeparatorInset: CGFloat = hasNext ? leftInset : 0.0 + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: bottomSeparatorInset, y: height - UIScreenPixel), size: CGSize(width: width - bottomSeparatorInset, height: UIScreenPixel))) + + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -(hasPrevious ? UIScreenPixel : 0.0)), size: CGSize(width: width, height: height + (hasPrevious ? UIScreenPixel : 0.0)))) + + if let image = self.disclosureNode.image { + self.disclosureNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - image.size.width, y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + return height + } + + @objc private func buttonPressed() { + self.selected() + } +} diff --git a/TelegramUI/SecureIdAuthPasswordOptionContentNode.swift b/TelegramUI/SecureIdAuthPasswordOptionContentNode.swift index 9dfd6ff9fb..e0db1f5626 100644 --- a/TelegramUI/SecureIdAuthPasswordOptionContentNode.swift +++ b/TelegramUI/SecureIdAuthPasswordOptionContentNode.swift @@ -45,6 +45,7 @@ final class SecureIdAuthPasswordOptionContentNode: ASDisplayNode, SecureIdAuthCo self.inputField.textField.font = passwordFont self.inputField.textField.textColor = theme.list.freeInputField.primaryColor self.inputField.textField.attributedPlaceholder = NSAttributedString(string: hint.isEmpty ? strings.LoginPassword_PasswordPlaceholder : hint, font: passwordFont, textColor: theme.list.freeInputField.placeholderColor) + self.inputField.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance self.buttonNode = HighlightableButtonNode() diff --git a/TelegramUI/SecureIdDocumentFormController.swift b/TelegramUI/SecureIdDocumentFormController.swift index 53a621f5dc..edcbfa1938 100644 --- a/TelegramUI/SecureIdDocumentFormController.swift +++ b/TelegramUI/SecureIdDocumentFormController.swift @@ -18,18 +18,16 @@ final class SecureIdDocumentFormController: FormController Void) { + init(account: Account, context: SecureIdAccessContext, requestedData: SecureIdDocumentFormRequestedData, values: [SecureIdValueWithContext], updatedValues: @escaping ([SecureIdValueWithContext]) -> Void) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.context = context self.requestedData = requestedData self.values = values self.updatedValues = updatedValues - self.errors = errors super.init(initParams: SecureIdDocumentFormControllerNodeInitParams(account: account, context: context), presentationData: self.presentationData) @@ -39,6 +37,8 @@ final class SecureIdDocumentFormController: FormController Bool { if !self.documentState.isEqual(to: to.documentState) { @@ -313,6 +333,12 @@ struct SecureIdDocumentFormState: FormControllerInnerState { if self.selfieDocument != to.selfieDocument { return false } + if self.frontSideDocument != to.frontSideDocument { + return false + } + if self.backSideDocument != to.backSideDocument { + return false + } return true } @@ -320,62 +346,293 @@ struct SecureIdDocumentFormState: FormControllerInnerState { switch self.documentState { case let .identity(identity): var result: [FormControllerItemEntry] = [] - var errorIndex = 0 - if let errors = self.errors[.personalDetails], !errors.isEmpty { - result.append(.spacer) - for error in errors { - result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error))) - errorIndex += 1 - } - result.append(.spacer) - } - if let _ = identity.document { + if let document = identity.document, false { result.append(.entry(SecureIdDocumentFormEntry.scansHeader)) + + let filesType: SecureIdValueKey + switch document.type { + case .passport: + filesType = .passport + case .internalPassport: + filesType = .internalPassport + case .driversLicense: + filesType = .driversLicense + case .idCard: + filesType = .idCard + } + + if let value = self.previousValues[filesType] { + var fileHashes: Set? = Set() + loop: for document in self.documents { + switch document { + case .local: + fileHashes = nil + break loop + case let .remote(file): + fileHashes?.insert(file.fileHash) + } + } + + if let fileHashes = fileHashes, !fileHashes.isEmpty, let error = value.errors[.files(hashes: fileHashes)] { + //result.append(.spacer) + result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error))) + errorIndex += 1 + } + } + for i in 0 ..< self.documents.count { - result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i]))) + var error: String? + switch self.documents[i] { + case .local: + break + case let .remote(file): + switch self.documentState { + case let .identity(identity): + if let document = identity.document { + switch document.type { + case .passport: + error = self.previousValues[.passport]?.errors[.file(hash: file.fileHash)] + case .internalPassport: + error = self.previousValues[.internalPassport]?.errors[.file(hash: file.fileHash)] + case .driversLicense: + error = self.previousValues[.driversLicense]?.errors[.file(hash: file.fileHash)] + case .idCard: + error = self.previousValues[.idCard]?.errors[.file(hash: file.fileHash)] + } + } + case let .address(address): + if let document = address.document { + switch document { + case .passportRegistration: + error = self.previousValues[.passportRegistration]?.errors[.file(hash: file.fileHash)] + case .temporaryRegistration: + error = self.previousValues[.temporaryRegistration]?.errors[.file(hash: file.fileHash)] + case .bankStatement: + error = self.previousValues[.bankStatement]?.errors[.file(hash: file.fileHash)] + case .utilityBill: + error = self.previousValues[.utilityBill]?.errors[.file(hash: file.fileHash)] + case .rentalAgreement: + error = self.previousValues[.rentalAgreement]?.errors[.file(hash: file.fileHash)] + } + } + } + } + result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i], error))) } result.append(.entry(SecureIdDocumentFormEntry.addScan(!self.documents.isEmpty))) result.append(.entry(SecureIdDocumentFormEntry.scansInfo(.identity))) result.append(.spacer) } - if self.selfieRequired { - result.append(.entry(SecureIdDocumentFormEntry.selfieHeader)) - if let document = self.selfieDocument { - result.append(.entry(SecureIdDocumentFormEntry.selfie(0, document))) - } - result.append(.entry(SecureIdDocumentFormEntry.addSelfie)) - result.append(.entry(SecureIdDocumentFormEntry.selfieInfo)) - result.append(.spacer) - } - if let details = identity.details { + result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) - result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName))) - result.append(.entry(SecureIdDocumentFormEntry.lastName(details.lastName))) - result.append(.entry(SecureIdDocumentFormEntry.gender(details.gender))) - result.append(.entry(SecureIdDocumentFormEntry.birthdate(details.birthdate))) - result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode))) + result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstName))]))) + result.append(.entry(SecureIdDocumentFormEntry.lastName(details.lastName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.lastName))]))) + + result.append(.entry(SecureIdDocumentFormEntry.birthdate(details.birthdate, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.birthdate))]))) + result.append(.entry(SecureIdDocumentFormEntry.gender(details.gender, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.gender))]))) + result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.countryCode))]))) + result.append(.entry(SecureIdDocumentFormEntry.residenceCountryCode(details.residenceCountryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.residenceCountryCode))]))) } if let document = identity.document { - result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier))) - result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate))) - if !self.previousValues.isEmpty { - result.append(.spacer) - result.append(.entry(SecureIdDocumentFormEntry.deleteDocument)) + if (identity.details == nil) { + result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) } + + var identifierError: String? + var expiryDateError: String? + + switch document.type { + case .passport: + identifierError = self.previousValues[.passport]?.errors[.field(.passport(.documentId))] + expiryDateError = self.previousValues[.passport]?.errors[.field(.passport(.expiryDate))] + case .internalPassport: + identifierError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.documentId))] + expiryDateError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.expiryDate))] + case .driversLicense: + identifierError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.documentId))] + expiryDateError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.expiryDate))] + case .idCard: + identifierError = self.previousValues[.idCard]?.errors[.field(.idCard(.documentId))] + expiryDateError = self.previousValues[.idCard]?.errors[.field(.idCard(.expiryDate))] + } + result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier, identifierError))) + result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate, expiryDateError))) + } + + if self.selfieRequired || self.frontSideRequired || self.backSideRequired { + result.append(.spacer) + result.append(.entry(SecureIdDocumentFormEntry.requestedDocumentsHeader)) + if self.frontSideRequired { + if let document = self.frontSideDocument { + var error: String? + if case let .remote(file) = document { + switch self.documentState { + case let .identity(identity): + if let document = identity.document { + switch document.type { + case .passport: + error = self.previousValues[.passport]?.errors[.frontSide(hash: file.fileHash)] + case .internalPassport: + error = self.previousValues[.internalPassport]?.errors[.frontSide(hash: file.fileHash)] + case .driversLicense: + error = self.previousValues[.driversLicense]?.errors[.frontSide(hash: file.fileHash)] + case .idCard: + error = self.previousValues[.idCard]?.errors[.frontSide(hash: file.fileHash)] + } + } + case .address: + break + } + } + result.append(.entry(SecureIdDocumentFormEntry.frontSide(1, document, error))) + } else { + result.append(.entry(SecureIdDocumentFormEntry.frontSide(1, nil, nil))) + } + } + if self.backSideRequired { + if let document = self.backSideDocument { + var error: String? + if case let .remote(file) = document { + switch self.documentState { + case let .identity(identity): + if let document = identity.document { + switch document.type { + case .passport: + error = self.previousValues[.passport]?.errors[.backSide(hash: file.fileHash)] + case .internalPassport: + error = self.previousValues[.internalPassport]?.errors[.backSide(hash: file.fileHash)] + case .driversLicense: + error = self.previousValues[.driversLicense]?.errors[.backSide(hash: file.fileHash)] + case .idCard: + error = self.previousValues[.idCard]?.errors[.backSide(hash: file.fileHash)] + } + } + case .address: + break + } + } + result.append(.entry(SecureIdDocumentFormEntry.backSide(2, document, error))) + } else { + result.append(.entry(SecureIdDocumentFormEntry.backSide(2, nil, nil))) + } + } + + if self.selfieRequired { + if let document = self.selfieDocument { + var error: String? + if case let .remote(file) = document { + switch self.documentState { + case let .identity(identity): + if let document = identity.document { + switch document.type { + case .passport: + error = self.previousValues[.passport]?.errors[.selfie(hash: file.fileHash)] + case .internalPassport: + error = self.previousValues[.internalPassport]?.errors[.selfie(hash: file.fileHash)] + case .driversLicense: + error = self.previousValues[.driversLicense]?.errors[.selfie(hash: file.fileHash)] + case .idCard: + error = self.previousValues[.idCard]?.errors[.selfie(hash: file.fileHash)] + } + } + case .address: + break + } + } + result.append(.entry(SecureIdDocumentFormEntry.selfie(0, document, error))) + } else { + result.append(.entry(SecureIdDocumentFormEntry.selfie(0, nil, nil))) + } + } + } + + if !self.previousValues.isEmpty { + result.append(.spacer) + result.append(.entry(SecureIdDocumentFormEntry.deleteDocument)) } return result case let .address(address): var result: [FormControllerItemEntry] = [] - if let _ = address.document { + var errorIndex = 0 + if let document = address.document { result.append(.entry(SecureIdDocumentFormEntry.scansHeader)) + + let filesType: SecureIdValueKey + switch document { + case .passportRegistration: + filesType = .passportRegistration + case .temporaryRegistration: + filesType = .temporaryRegistration + case .bankStatement: + filesType = .bankStatement + case .rentalAgreement: + filesType = .rentalAgreement + case .utilityBill: + filesType = .utilityBill + } + + if let value = self.previousValues[filesType] { + var fileHashes: Set? = Set() + loop: for document in self.documents { + switch document { + case .local: + fileHashes = nil + break loop + case let .remote(file): + fileHashes?.insert(file.fileHash) + } + } + + if let fileHashes = fileHashes, !fileHashes.isEmpty, let error = value.errors[.files(hashes: fileHashes)] { + result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error))) + errorIndex += 1 + } + } + for i in 0 ..< self.documents.count { - result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i]))) + var error: String? + switch self.documents[i] { + case .local: + break + case let .remote(file): + switch self.documentState { + case let .identity(identity): + if let document = identity.document { + switch document.type { + case .passport: + error = self.previousValues[.passport]?.errors[.file(hash: file.fileHash)] + case .internalPassport: + error = self.previousValues[.internalPassport]?.errors[.file(hash: file.fileHash)] + case .driversLicense: + error = self.previousValues[.driversLicense]?.errors[.file(hash: file.fileHash)] + case .idCard: + error = self.previousValues[.idCard]?.errors[.file(hash: file.fileHash)] + } + } + case let .address(address): + if let document = address.document { + switch document { + case .passportRegistration: + error = self.previousValues[.passportRegistration]?.errors[.file(hash: file.fileHash)] + case .temporaryRegistration: + error = self.previousValues[.temporaryRegistration]?.errors[.file(hash: file.fileHash)] + case .bankStatement: + error = self.previousValues[.bankStatement]?.errors[.file(hash: file.fileHash)] + case .utilityBill: + error = self.previousValues[.utilityBill]?.errors[.file(hash: file.fileHash)] + case .rentalAgreement: + error = self.previousValues[.rentalAgreement]?.errors[.file(hash: file.fileHash)] + } + } + } + } + result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i], error))) } result.append(.entry(SecureIdDocumentFormEntry.addScan(!self.documents.isEmpty))) result.append(.entry(SecureIdDocumentFormEntry.scansInfo(.address))) @@ -383,13 +640,14 @@ struct SecureIdDocumentFormState: FormControllerInnerState { } if let details = address.details { + result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.address))) - result.append(.entry(SecureIdDocumentFormEntry.street1(details.street1))) - result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2))) - result.append(.entry(SecureIdDocumentFormEntry.city(details.city))) - result.append(.entry(SecureIdDocumentFormEntry.state(details.state))) - result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode))) - result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode))) + result.append(.entry(SecureIdDocumentFormEntry.street1(details.street1, self.previousValues[.address]?.errors[.field(.address(.streetLine1))]))) + result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2, self.previousValues[.address]?.errors[.field(.address(.streetLine2))]))) + result.append(.entry(SecureIdDocumentFormEntry.city(details.city, self.previousValues[.address]?.errors[.field(.address(.city))]))) + result.append(.entry(SecureIdDocumentFormEntry.state(details.state, self.previousValues[.address]?.errors[.field(.address(.state))]))) + result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode, self.previousValues[.address]?.errors[.field(.address(.countryCode))]))) + result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode, self.previousValues[.address]?.errors[.field(.address(.postCode))]))) } if !self.previousValues.isEmpty { @@ -457,7 +715,7 @@ struct SecureIdDocumentFormState: FormControllerInnerState { } extension SecureIdDocumentFormState { - init(requestedData: SecureIdDocumentFormRequestedData, values: [SecureIdValueKey: SecureIdValueWithContext], errors: [SecureIdErrorKey: [String]]) { + init(requestedData: SecureIdDocumentFormRequestedData, values: [SecureIdValueKey: SecureIdValueWithContext]) { switch requestedData { case let .identity(details, document, selfie): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] @@ -465,14 +723,18 @@ extension SecureIdDocumentFormState { if details { if let value = values[.personalDetails], case let .personalDetails(personalDetailsValue) = value.value { previousValues[.personalDetails] = value - detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: personalDetailsValue.firstName, lastName: personalDetailsValue.lastName, countryCode: personalDetailsValue.countryCode, birthdate: personalDetailsValue.birthdate, gender: personalDetailsValue.gender) + detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: personalDetailsValue.firstName, lastName: personalDetailsValue.lastName, countryCode: personalDetailsValue.countryCode, residenceCountryCode: personalDetailsValue.residenceCountryCode, birthdate: personalDetailsValue.birthdate, gender: personalDetailsValue.gender) } else { - detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: "", lastName: "", countryCode: "", birthdate: nil, gender: nil) + detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: "", lastName: "", countryCode: "", residenceCountryCode: "", birthdate: nil, gender: nil) } } var documentState: SecureIdDocumentFormIdentityDocumentState? var verificationDocuments: [SecureIdVerificationDocument] = [] var selfieDocument: SecureIdVerificationDocument? + var frontSideRequired: Bool = false + var backSideRequired: Bool = false + var frontSideDocument: SecureIdVerificationDocument? + var backSideDocument: SecureIdVerificationDocument? if let document = document { var identifier: String = "" var expiryDate: SecureIdDate? @@ -485,6 +747,17 @@ extension SecureIdDocumentFormState { verificationDocuments = passport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = passport.selfieDocument.flatMap(SecureIdVerificationDocument.init) } + frontSideRequired = true + case .internalPassport: + if let value = values[.internalPassport], case let .internalPassport(internalPassport) = value.value { + previousValues[value.value.key] = value + identifier = internalPassport.identifier + expiryDate = internalPassport.expiryDate + verificationDocuments = internalPassport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) + selfieDocument = internalPassport.selfieDocument.flatMap(SecureIdVerificationDocument.init) + frontSideDocument = internalPassport.frontSideDocument.flatMap(SecureIdVerificationDocument.init) + } + frontSideRequired = true case .driversLicense: if let value = values[.driversLicense], case let .driversLicense(driversLicense) = value.value { previousValues[value.value.key] = value @@ -492,7 +765,11 @@ extension SecureIdDocumentFormState { expiryDate = driversLicense.expiryDate verificationDocuments = driversLicense.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = driversLicense.selfieDocument.flatMap(SecureIdVerificationDocument.init) + frontSideDocument = driversLicense.frontSideDocument.flatMap(SecureIdVerificationDocument.init) + backSideDocument = driversLicense.backSideDocument.flatMap(SecureIdVerificationDocument.init) } + frontSideRequired = true + backSideRequired = true case .idCard: if let value = values[.idCard], case let .idCard(idCard) = value.value { previousValues[value.value.key] = value @@ -500,12 +777,16 @@ extension SecureIdDocumentFormState { expiryDate = idCard.expiryDate verificationDocuments = idCard.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = idCard.selfieDocument.flatMap(SecureIdVerificationDocument.init) + frontSideDocument = idCard.frontSideDocument.flatMap(SecureIdVerificationDocument.init) + backSideDocument = idCard.backSideDocument.flatMap(SecureIdVerificationDocument.init) } + frontSideRequired = true + backSideRequired = true } documentState = SecureIdDocumentFormIdentityDocumentState(type: document, identifier: identifier, expiryDate: expiryDate) } let formState = SecureIdDocumentFormIdentityState(details: detailsState, document: documentState) - self.init(previousValues: previousValues, documentState: .identity(formState), documents: verificationDocuments, selfieRequired: selfie, selfieDocument: selfieDocument, actionState: .none, errors: errors) + self.init(previousValues: previousValues, documentState: .identity(formState), documents: verificationDocuments, selfieRequired: selfie, selfieDocument: selfieDocument, frontSideRequired: frontSideRequired, frontSideDocument: frontSideDocument, backSideRequired: backSideRequired, backSideDocument: backSideDocument, actionState: .none) case let .address(details, document): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] var detailsState: SecureIdDocumentFormAddressDetailsState? @@ -522,6 +803,16 @@ extension SecureIdDocumentFormState { } if let document = document { switch document { + case .passportRegistration: + if let value = values[.passportRegistration], case let .passportRegistration(passportRegistration) = value.value { + previousValues[value.value.key] = value + verificationDocuments = passportRegistration.verificationDocuments.compactMap(SecureIdVerificationDocument.init) + } + case .temporaryRegistration: + if let value = values[.temporaryRegistration], case let .temporaryRegistration(temporaryRegistration) = value.value { + previousValues[value.value.key] = value + verificationDocuments = temporaryRegistration.verificationDocuments.compactMap(SecureIdVerificationDocument.init) + } case .bankStatement: if let value = values[.bankStatement], case let .bankStatement(bankStatement) = value.value { previousValues[value.value.key] = value @@ -541,7 +832,7 @@ extension SecureIdDocumentFormState { documentState = document } let formState = SecureIdDocumentFormAddressState(details: detailsState, document: documentState) - self.init(previousValues: previousValues, documentState: .address(formState), documents: verificationDocuments, selfieRequired: false, selfieDocument: nil, actionState: .none, errors: errors) + self.init(previousValues: previousValues, documentState: .address(formState), documents: verificationDocuments, selfieRequired: false, selfieDocument: nil, frontSideRequired: false, frontSideDocument: nil, backSideRequired: false, backSideDocument: nil, actionState: .none) } } @@ -574,6 +865,34 @@ extension SecureIdDocumentFormState { } } } + var frontSideDocument: SecureIdVerificationDocumentReference? + if let document = self.frontSideDocument { + switch document { + case let .remote(file): + frontSideDocument = .remote(file) + case let .local(file): + switch file.state { + case let .uploaded(file): + frontSideDocument = .uploaded(file) + case .uploading: + return nil + } + } + } + var backSideDocument: SecureIdVerificationDocumentReference? + if let document = self.backSideDocument { + switch document { + case let .remote(file): + backSideDocument = .remote(file) + case let .local(file): + switch file.state { + case let .uploaded(file): + backSideDocument = .uploaded(file) + case .uploading: + return nil + } + } + } switch self.documentState { case let .identity(identity): @@ -588,13 +907,16 @@ extension SecureIdDocumentFormState { guard !details.countryCode.isEmpty else { return nil } + guard !details.residenceCountryCode.isEmpty else { + return nil + } guard let birthdate = details.birthdate else { return nil } guard let gender = details.gender else { return nil } - values[.personalDetails] = .personalDetails(SecureIdPersonalDetailsValue(firstName: details.firstName, lastName: details.lastName, birthdate: birthdate, countryCode: details.countryCode, gender: gender)) + values[.personalDetails] = .personalDetails(SecureIdPersonalDetailsValue(firstName: details.firstName, lastName: details.lastName, birthdate: birthdate, countryCode: details.countryCode, residenceCountryCode: details.residenceCountryCode, gender: gender)) } if let document = identity.document { guard !document.identifier.isEmpty else { @@ -603,11 +925,13 @@ extension SecureIdDocumentFormState { switch document.type { case .passport: - values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument)) + values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) + case .internalPassport: + values[.internalPassport] = .internalPassport(SecureIdInternalPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) case .driversLicense: - values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument)) + values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument, backSideDocument: backSideDocument)) case .idCard: - values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument)) + values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument, backSideDocument: backSideDocument)) } } return values @@ -630,6 +954,10 @@ extension SecureIdDocumentFormState { } if let document = address.document { switch document { + case .passportRegistration: + values[.passportRegistration] = .passportRegistration(SecureIdPassportRegistrationValue(verificationDocuments: verificationDocuments)) + case .temporaryRegistration: + values[.temporaryRegistration] = .temporaryRegistration(SecureIdTemporaryRegistrationValue(verificationDocuments: verificationDocuments)) case .bankStatement: values[.bankStatement] = .bankStatement(SecureIdBankStatementValue(verificationDocuments: verificationDocuments)) case .utilityBill: @@ -654,13 +982,15 @@ enum SecureIdDocumentFormEntryId: Hashable { case lastName case gender case countryCode + case residenceCountryCode case birthdate case expiryDate case deleteDocument - case selfieHeader + case requestedDocumentsHeader case selfie - case addSelfie - case selfieInfo + case frontSide + case backSide + case documentsInfo case street1 case street2 @@ -678,35 +1008,37 @@ enum SecureIdDocumentFormEntryCategory { enum SecureIdDocumentFormEntry: FormControllerEntry { case scansHeader - case scan(Int, SecureIdVerificationDocument) + case scan(Int, SecureIdVerificationDocument, String?) case addScan(Bool) case scansInfo(SecureIdDocumentFormEntryCategory) case infoHeader(SecureIdDocumentFormEntryCategory) - case identifier(String) - case firstName(String) - case lastName(String) - case gender(SecureIdGender?) - case countryCode(String) - case birthdate(SecureIdDate?) - case expiryDate(SecureIdDate?) + case identifier(String, String?) + case firstName(String, String?) + case lastName(String, String?) + case gender(SecureIdGender?, String?) + case countryCode(String, String?) + case residenceCountryCode(String, String?) + case birthdate(SecureIdDate?, String?) + case expiryDate(SecureIdDate?, String?) case deleteDocument - case selfieHeader - case selfie(Int, SecureIdVerificationDocument) - case addSelfie - case selfieInfo + case requestedDocumentsHeader + case selfie(Int, SecureIdVerificationDocument?, String?) + case frontSide(Int, SecureIdVerificationDocument?, String?) + case backSide(Int, SecureIdVerificationDocument?, String?) + case documentsInfo case error(Int, String) - case street1(String) - case street2(String) - case city(String) - case state(String) - case postcode(String) + case street1(String, String?) + case street2(String, String?) + case city(String, String?) + case state(String, String?) + case postcode(String, String?) var stableId: SecureIdDocumentFormEntryId { switch self { case .scansHeader: return .scansHeader - case let .scan(_, document): + case let .scan(_, document, _): return .scan(document.id) case .addScan: return .addScan @@ -722,6 +1054,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { return .lastName case .countryCode: return .countryCode + case .residenceCountryCode: + return .residenceCountryCode case .birthdate: return .birthdate case .expiryDate: @@ -740,14 +1074,16 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { return .postcode case .gender: return .gender - case .selfieHeader: - return .selfieHeader + case .requestedDocumentsHeader: + return .requestedDocumentsHeader case .selfie: return .selfie - case .addSelfie: - return .addSelfie - case .selfieInfo: - return .selfieInfo + case .frontSide: + return .frontSide + case .backSide: + return .backSide + case .documentsInfo: + return .documentsInfo case .error: return .error } @@ -761,8 +1097,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { } else { return false } - case let .scan(lhsId, lhsDocument): - if case let .scan(rhsId, rhsDocument) = to, lhsId == rhsId, lhsDocument == rhsDocument { + case let .scan(lhsId, lhsDocument, lhsError): + if case let .scan(rhsId, rhsDocument, rhsError) = to, lhsId == rhsId, lhsDocument == rhsDocument, lhsError == rhsError { return true } else { return false @@ -785,44 +1121,50 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { } else { return false } - case let .identifier(value): - if case .identifier(value) = to { + case let .identifier(value, error): + if case .identifier(value, error) = to { return true } else { return false } - case let .firstName(value): - if case .firstName(value) = to { + case let .firstName(value, error): + if case .firstName(value, error) = to { return true } else { return false } - case let .lastName(value): - if case .lastName(value) = to { + case let .lastName(value, error): + if case .lastName(value, error) = to { return true } else { return false } - case let .gender(value): - if case .gender(value) = to { + case let .gender(value, error): + if case .gender(value, error) = to { return true } else { return false } - case let .countryCode(value): - if case .countryCode(value) = to { + case let .countryCode(value, error): + if case .countryCode(value, error) = to { return true } else { return false } - case let .birthdate(lhsValue): - if case let .birthdate(rhsValue) = to, lhsValue == rhsValue { + case let .residenceCountryCode(value, error): + if case .residenceCountryCode(value, error) = to { return true } else { return false } - case let .expiryDate(lhsValue): - if case let .expiryDate(rhsValue) = to, lhsValue == rhsValue { + case let .birthdate(lhsValue, lhsError): + if case let .birthdate(rhsValue, rhsError) = to, lhsValue == rhsValue, lhsError == rhsError { + return true + } else { + return false + } + case let .expiryDate(lhsValue, lhsError): + if case let .expiryDate(rhsValue, rhsError) = to, lhsValue == rhsValue, lhsError == rhsError { return true } else { return false @@ -833,62 +1175,68 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { } else { return false } - case let .street1(value): - if case .street1(value) = to { + case let .street1(value, error): + if case .street1(value, error) = to { return true } else { return false } - case let .street2(value): - if case .street2(value) = to { + case let .street2(value, error): + if case .street2(value, error) = to { return true } else { return false } - case let .city(value): - if case .city(value) = to { + case let .city(value, error): + if case .city(value, error) = to { return true } else { return false } - case let .state(value): - if case .state(value) = to { + case let .state(value, error): + if case .state(value, error) = to { return true } else { return false } - case let .postcode(value): - if case .postcode(value) = to { + case let .postcode(value, error): + if case .postcode(value, error) = to { return true } else { return false } - case .selfieHeader: - if case .selfieHeader = to { + case .requestedDocumentsHeader: + if case .requestedDocumentsHeader = to { return true } else { return false } - case let .selfie(index, document): - if case .selfie(index, document) = to { + case let .selfie(index, document, error): + if case .selfie(index, document, error) = to { return true } else { return false } - case .addSelfie: - if case .addSelfie = to { + case let .frontSide(index, document, error): + if case .frontSide(index, document, error) = to { return true } else { return false } - case .selfieInfo: - if case .selfieInfo = to { + case let .backSide(index, document, error): + if case .backSide(index, document, error) = to { return true } else { return false } - case .error: - if case .error = to { + case .documentsInfo: + if case .documentsInfo = to { + return true + } else { + return false + } + case let .error(index, text): + if case .error(index, text) = to { return true } else { return false @@ -900,12 +1248,12 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { switch self { case .scansHeader: return FormControllerHeaderItem(text: "SCANS") - case let .scan(index, document): - return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, title: "Scan \(index + 1)", activated: { + case let .scan(index, document, error): + return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: nil, title: "Scan \(index + 1)", label: error.flatMap(SecureIdValueFormFileItemLabel.error) ?? .timestamp, activated: { params.openDocument(document) }) case let .addScan(hasAny): - return FormControllerActionItem(type: .accent, title: hasAny ? "Upload More Scans" : "Upload Scan", fullTopInset: true, activated: { + return FormControllerActionItem(type: .accent, title: hasAny ? "Upload Additional Scan" : "Upload Scan", fullTopInset: true, activated: { params.addFile(.scan) }) case let .scansInfo(type): @@ -914,7 +1262,7 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { case .identity: text = "The document must contain your photograph, name, surname, date of birth, citizenship, document issue date and document number." case .address: - text = "The scans must contain proof of address." + text = "The document must contain your first and last name, your residential address, a stamp / barcode / QR code / logo, and issue date, no more that 3 months ago." } return FormControllerTextItem(text: text) case let .infoHeader(type): @@ -926,19 +1274,19 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { text = "ADDRESS" } return FormControllerHeaderItem(text: text) - case let .identifier(value): - return FormControllerTextInputItem(title: "ID", text: value, placeholder: "ID", textUpdated: { text in + case let .identifier(value, error): + return FormControllerTextInputItem(title: "Document #", text: value, placeholder: "Document Number", error: error, textUpdated: { text in params.updateText(.identifier, text) }) - case let .firstName(value): - return FormControllerTextInputItem(title: "First Name", text: value, placeholder: "First Name", textUpdated: { text in + case let .firstName(value, error): + return FormControllerTextInputItem(title: "First Name", text: value, placeholder: "First Name", error: error, textUpdated: { text in params.updateText(.firstName, text) }) - case let .lastName(value): - return FormControllerTextInputItem(title: "Last Name", text: value, placeholder: "Last Name", textUpdated: { text in + case let .lastName(value, error): + return FormControllerTextInputItem(title: "Last Name", text: value, placeholder: "Last Name", error: error, textUpdated: { text in params.updateText(.lastName, text) }) - case let .gender(value): + case let .gender(value, error): var text = "" if let value = value { switch value { @@ -948,57 +1296,101 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { text = "Female" } } - return FormControllerDetailActionItem(title: "Gender", text: text, placeholder: "Gender", activated: { + return FormControllerDetailActionItem(title: "Gender", text: text, placeholder: "Gender", error: error, activated: { params.activateSelection(.gender) }) - case let .countryCode(value): - return FormControllerDetailActionItem(title: "Country", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Country", activated: { + case let .countryCode(value, error): + return FormControllerDetailActionItem(title: "Country", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Country", error: error, activated: { params.activateSelection(.country) }) - case let .birthdate(value): - return FormControllerDetailActionItem(title: "Date of Birth", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Date of Birth", activated: { + case let .residenceCountryCode(value, error): + return FormControllerDetailActionItem(title: "Residence", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Residence Country", error: error, activated: { + params.activateSelection(.residenceCountry) + }) + case let .birthdate(value, error): + return FormControllerDetailActionItem(title: "Date of Birth", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Date of Birth", error: error, activated: { params.activateSelection(.date(value?.timestamp, .birthdate)) }) - case let .expiryDate(value): - return FormControllerDetailActionItem(title: "Expires", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Expires", activated: { + case let .expiryDate(value, error): + return FormControllerDetailActionItem(title: "Expiry Date", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Expiry Date", error: error, activated: { params.activateSelection(.date(value?.timestamp, .expiry)) }) case .deleteDocument: return FormControllerActionItem(type: .destructive, title: "Delete Document", activated: { params.deleteValue() }) - case let .street1(value): - return FormControllerTextInputItem(title: "Street 1", text: value, placeholder: "Street 1", textUpdated: { text in + case let .street1(value, error): + return FormControllerTextInputItem(title: "Street", text: value, placeholder: "Street and number, P.O. box", error: error, textUpdated: { text in params.updateText(.street1, text) }) - case let .street2(value): - return FormControllerTextInputItem(title: "Street 2", text: value, placeholder: "Street 2", textUpdated: { text in + case let .street2(value, error): + return FormControllerTextInputItem(title: "", text: value, placeholder: "Apt., suite, unit, builting, block", error: error, textUpdated: { text in params.updateText(.street2, text) }) - case let .city(value): - return FormControllerTextInputItem(title: "City", text: value, placeholder: "City", textUpdated: { text in + case let .city(value, error): + return FormControllerTextInputItem(title: "City", text: value, placeholder: "City", error: error, textUpdated: { text in params.updateText(.city, text) }) - case let .state(value): - return FormControllerTextInputItem(title: "State", text: value, placeholder: "State", textUpdated: { text in + case let .state(value, error): + return FormControllerTextInputItem(title: "Region", text: value, placeholder: "State / Province / Region", error: error, textUpdated: { text in params.updateText(.state, text) }) - case let .postcode(value): - return FormControllerTextInputItem(title: "Postcode", text: value, placeholder: "Postcode", textUpdated: { text in + case let .postcode(value, error): + return FormControllerTextInputItem(title: "Postcode", text: value, placeholder: "Postcode", error: error, textUpdated: { text in params.updateText(.postcode, text) }) - case .selfieHeader: - return FormControllerHeaderItem(text: "SELFIE") - case let .selfie(_, document): - return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, title: "Selfie", activated: { - params.openDocument(document) + case .requestedDocumentsHeader: + return FormControllerHeaderItem(text: "REQUESTED FILES") + case let .selfie(_, document, error): + let label: SecureIdValueFormFileItemLabel + if let error = error { + label = .error(error) + } else if document != nil { + label = .timestamp + } else { + label = .text("Upload a selfie of yourself holding document") + } + return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputSelfie"), title: "Selfie", label: label, activated: { + if let document = document { + params.openDocument(document) + } else { + params.addFile(.selfie) + } }) - case .addSelfie: - return FormControllerActionItem(type: .accent, title: "Upload Selfie", fullTopInset: true, activated: { - params.addFile(.selfie) + case let .frontSide(_, document, error): + let label: SecureIdValueFormFileItemLabel + if let error = error { + label = .error(error) + } else if document != nil { + label = .timestamp + } else { + label = .text("Upload a front side photo of a document") + } + return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/PassportInputFrontSide"), title: "Front Side", label: label, activated: { + if let document = document { + params.openDocument(document) + } else { + params.addFile(.frontSide) + } }) - case .selfieInfo: - return FormControllerTextItem(text: "Take a selfie picture with youself holding the document.") + case let .backSide(_, document, error): + let label: SecureIdValueFormFileItemLabel + if let error = error { + label = .error(error) + } else if document != nil { + label = .timestamp + } else { + label = .text("Upload a reverse side photo of a document") + } + return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputBackSide"), title: "Reverse Side", label: label, activated: { + if let document = document { + params.openDocument(document) + } else { + params.addFile(.backSide) + } + }) + case .documentsInfo: + return FormControllerTextItem(text: "") case let .error(_, text): return FormControllerTextItem(text: text, color: .error) } @@ -1056,6 +1448,60 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void = { [weak controller] in controller?.dismissAnimated() } + let applyAction: (SecureIdGender) -> Void = { gender in + if let strongSelf = self, var innerState = strongSelf.innerState { + innerState.documentState.updateGenderField(type: .gender, value: gender) + var valueKey: SecureIdValueKey? + var errorKey: SecureIdValueContentErrorKey? + valueKey = .personalDetails + errorKey = .field(.personalDetails(.gender)) + if let valueKey = valueKey, let errorKey = errorKey { + if let previousValue = innerState.previousValues[valueKey] { + innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) + } + } + strongSelf.updateInnerState(transition: .immediate, with: innerState) + } + } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: "Male", action: { dismissAction() - if let strongSelf = self, var innerState = strongSelf.innerState { - innerState.documentState.updateGenderField(type: .gender, value: .male) - strongSelf.updateInnerState(transition: .immediate, with: innerState) - } + applyAction(.male) }), ActionSheetButtonItem(title: "Female", action: { dismissAction() - if let strongSelf = self, var innerState = strongSelf.innerState { - innerState.documentState.updateGenderField(type: .gender, value: .female) - strongSelf.updateInnerState(transition: .immediate, with: innerState) - } + applyAction(.female) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, action: { dismissAction() })]) @@ -1143,6 +1672,28 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode deliverOnMainQueue).start(next: { resources in - if let strongSelf = self { - strongSelf.addDocuments(type: type, resources: resources) - } - }) - } - } - controller.dismissalBlock = { [weak legacyController] in - if let legacyController = legacyController { - legacyController.dismiss() - } - } - strongSelf.view.endEditing(true) - strongSelf.present(legacyController, nil) - } + guard let validLayout = self.layoutState?.layout else { + return + } + let attachmentType: SecureIdAttachmentMenuType + switch type { + case .scan: + attachmentType = .multiple + case .backSide, .frontSide: + attachmentType = .generic + case .selfie: + attachmentType = .selfie + } + presentLegacySecureIdAttachmentMenu(account: self.account, present: { [weak self] c in + self?.present(c, nil) + }, validLayout: validLayout, type: attachmentType, completion: { [weak self] resources, recognizedData in + self?.addDocuments(type: type, resources: resources, recognizedData: recognizedData) }) } - private func addDocuments(type: AddFileTarget, resources: [TelegramMediaResource]) { + private func addDocuments(type: AddFileTarget, resources: [TelegramMediaResource], recognizedData: SecureIdRecognizedDocumentData?) { guard var innerState = self.innerState else { return } @@ -1202,14 +1731,77 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode deliverOnMainQueue).start(next: { [weak self] result in - if let strongSelf = self { - strongSelf.completedWithValues?([]) - } - }, error: { [weak self] error in + |> deliverOnMainQueue).start(error: { [weak self] error in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return @@ -1301,6 +1895,10 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode deliverOnMainQueue).start(next: { [weak self] entry in guard let strongSelf = self else { return } for itemNode in strongSelf.itemNodes { if let itemNode = itemNode as? SecureIdValueFormFileItemNode, let item = itemNode.item { - if let entry = entry, item.document.resource.isEqual(to: entry.resource) { + if let entry = entry, let document = item.document, document.resource.isEqual(to: entry.resource) { itemNode.imageNode.isHidden = true } else { itemNode.imageNode.isHidden = false @@ -1348,8 +1975,8 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Bool { - return lhs.index == rhs.index && lhs.resource.isEqual(to: rhs.resource) && lhs.location == rhs.location + return lhs.index == rhs.index && lhs.resource.isEqual(to: rhs.resource) && lhs.location == rhs.location && lhs.error == rhs.error } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, context: SecureIdAccessContext) -> GalleryItem { - return SecureIdDocumentGalleryItem(account: account, theme: theme, strings: strings, context: context, resource: resource, caption: "", location: self.location) + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, context: SecureIdAccessContext, delete: @escaping (TelegramMediaResource) -> Void) -> GalleryItem { + return SecureIdDocumentGalleryItem(account: account, theme: theme, strings: strings, context: context, resource: self.resource, caption: self.error, location: self.location, delete: { + delete(self.resource) + }) } } @@ -70,6 +73,8 @@ class SecureIdDocumentGalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void + var deleteResource: ((TelegramMediaResource) -> Void)? + init(account: Account, context: SecureIdAccessContext, entries: [SecureIdDocumentGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { self.account = account self.context = context @@ -92,7 +97,9 @@ class SecureIdDocumentGalleryController: ViewController { strongSelf.centralEntryIndex = centralIndex if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, context: strongSelf.context) + $0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, context: strongSelf.context, delete: { resource in + self?.deleteItem(resource) + }) }), centralItemIndex: centralIndex, keepFirst: false) let ready = (strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void()))) |> afterNext { [weak strongSelf] _ in @@ -247,7 +254,9 @@ class SecureIdDocumentGalleryController: ViewController { firstLayout = false self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, context: self.context) + $0.item(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, context: self.context, delete: { [weak self] resource in + self?.deleteItem(resource) + }) }), centralItemIndex: self.centralEntryIndex) let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in @@ -257,5 +266,10 @@ class SecureIdDocumentGalleryController: ViewController { self._ready.set(ready |> map { true }) } } + + private func deleteItem(_ resource: TelegramMediaResource) { + self.deleteResource?(resource) + self.dismiss(forceAway: true) + } } diff --git a/TelegramUI/SecureIdDocumentGalleryFooterContentNode.swift b/TelegramUI/SecureIdDocumentGalleryFooterContentNode.swift new file mode 100644 index 0000000000..63dad994a7 --- /dev/null +++ b/TelegramUI/SecureIdDocumentGalleryFooterContentNode.swift @@ -0,0 +1,156 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import Photos + +private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: .white) + +private let textFont = Font.regular(16.0) +private let titleFont = Font.medium(15.0) +private let dateFont = Font.regular(14.0) + +final class SecureIdDocumentGalleryFooterContentNode: GalleryFooterContentNode { + private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let deleteButton: UIButton + private let textNode: ASTextNode + private let authorNameNode: ASTextNode + private let dateNode: ASTextNode + + private var currentDateText: String? + private var currentMessageText: String? + private var currentDocument: SecureIdVerificationDocument? + + var delete: (() -> Void)? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + self.account = account + self.theme = theme + self.strings = strings + + self.deleteButton = UIButton() + + self.deleteButton.setImage(deleteImage, for: [.normal]) + + self.textNode = ASTextNode() + self.textNode.isLayerBacked = true + self.authorNameNode = ASTextNode() + self.authorNameNode.maximumNumberOfLines = 1 + self.authorNameNode.isLayerBacked = true + self.authorNameNode.displaysAsynchronously = false + self.dateNode = ASTextNode() + self.dateNode.maximumNumberOfLines = 1 + self.dateNode.isLayerBacked = true + self.dateNode.displaysAsynchronously = false + + super.init() + + self.view.addSubview(self.deleteButton) + self.addSubnode(self.textNode) + self.addSubnode(self.authorNameNode) + self.addSubnode(self.dateNode) + + self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) + } + + deinit { + } + + func setup(caption: String) { + let dateText: String? = nil// = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, timeFormat: .regular, timestamp: $0) } + + if self.currentMessageText != caption || self.currentDateText != dateText { + self.currentMessageText = caption + + if caption.isEmpty { + self.textNode.isHidden = true + self.textNode.attributedText = nil + } else { + self.textNode.isHidden = false + self.textNode.attributedText = NSAttributedString(string: caption, font: textFont, textColor: UIColor(rgb: 0xcf3030)) + } + + self.authorNameNode.attributedText = nil + if let dateText = dateText { + self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) + } else { + self.dateNode.attributedText = nil + } + + self.requestLayout?(.immediate) + } + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + var panelHeight: CGFloat = 44.0 + bottomInset + panelHeight += contentInset + if !self.textNode.isHidden { + let sideInset: CGFloat = 8.0 + leftInset + let topInset: CGFloat = 8.0 + let textBottomInset: CGFloat = 8.0 + contentInset + let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + panelHeight += textSize.height + topInset + textBottomInset + self.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize) + } + + self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) + + let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) + let dateSize = self.dateNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + if authorNameSize.height.isZero { + self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize) + } else { + let labelsSpacing: CGFloat = 0.0 + self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize) + self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) + } + + return panelHeight + } + + override func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) { + transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: 0.0, y: self.bounds.size.height - fromHeight)) + self.textNode.alpha = 1.0 + self.dateNode.alpha = 1.0 + self.authorNameNode.alpha = 1.0 + self.deleteButton.alpha = 1.0 + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + override func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + transition.updateFrame(node: self.textNode, frame: self.textNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight)) + self.textNode.alpha = 0.0 + self.dateNode.alpha = 0.0 + self.authorNameNode.alpha = 0.0 + self.deleteButton.alpha = 0.0 + self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { _ in + completion() + }) + } + + @objc func deleteButtonPressed() { + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.delete?() + }) + ] + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.controllerInteraction?.presentController(actionSheet, nil) + } +} diff --git a/TelegramUI/SecureIdDocumentImageGalleryItem.swift b/TelegramUI/SecureIdDocumentImageGalleryItem.swift index 48c283df54..839046054f 100644 --- a/TelegramUI/SecureIdDocumentImageGalleryItem.swift +++ b/TelegramUI/SecureIdDocumentImageGalleryItem.swift @@ -13,8 +13,9 @@ class SecureIdDocumentGalleryItem: GalleryItem { let resource: TelegramMediaResource let caption: String let location: SecureIdDocumentGalleryEntryLocation + let delete: () -> Void - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, context: SecureIdAccessContext, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, context: SecureIdAccessContext, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) { self.account = account self.theme = theme self.strings = strings @@ -22,6 +23,7 @@ class SecureIdDocumentGalleryItem: GalleryItem { self.resource = resource self.caption = caption self.location = location + self.delete = delete } func node() -> GalleryItemNode { @@ -32,6 +34,7 @@ class SecureIdDocumentGalleryItem: GalleryItem { node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)")) node.setCaption(self.caption) + node.delete = self.delete return node } @@ -41,6 +44,7 @@ class SecureIdDocumentGalleryItem: GalleryItem { node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)")) node.setCaption(self.caption) + node.delete = self.delete } } @@ -55,17 +59,23 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { private let imageNode: TransformImageNode fileprivate let _ready = Promise() fileprivate let _title = Promise() - //private let footerContentNode: SecureIdDocumentGalleryFooterContentNode + private let footerContentNode: SecureIdDocumentGalleryFooterContentNode private var accountAndMedia: (Account, SecureIdAccessContext, TelegramMediaResource)? private var fetchDisposable = MetaDisposable() + var delete: (() -> Void)? { + didSet { + self.footerContentNode.delete = self.delete + } + } + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account self.imageNode = TransformImageNode() - //self.footerContentNode = SecureIdDocumentGalleryFooterContentNode(account: account, theme: theme, strings: strings) + self.footerContentNode = SecureIdDocumentGalleryFooterContentNode(account: account, theme: theme, strings: strings) super.init() @@ -93,7 +103,7 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { } fileprivate func setCaption(_ caption: String) { - //self.footerContentNode.setCaption(caption) + self.footerContentNode.setup(caption: caption) } fileprivate func setResource(context: SecureIdAccessContext, resource: TelegramMediaResource) { @@ -201,8 +211,7 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { } override func footerContent() -> Signal { - //return .single(self.footerContentNode) - return .single(nil) + return .single(self.footerContentNode) } } diff --git a/TelegramUI/SecureIdErrors.swift b/TelegramUI/SecureIdErrors.swift deleted file mode 100644 index 05fa8ddd88..0000000000 --- a/TelegramUI/SecureIdErrors.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import TelegramCore - -enum SecureIdErrorCategory: Int32, Hashable { - case personalDetails - case passport - case driversLicense - case idCard - case address - case bankStatement - case utilityRecord - case rentalAgreement -} - -enum SecureIdErrorField: Int32, Hashable { - case personalDetails - case passport - case driversLicense - case idCard - case address - case bankStatement - case utilityRecord - case rentalAgreement -} - -struct SecureIdErrorKey1: Hashable { - let category: SecureIdErrorCategory - let field: SecureIdErrorField -} - -enum SecureIdErrorKey: Int32, Hashable { - case personalDetails -} - -func parseSecureIdErrors(_ string: String) -> [SecureIdErrorKey: [String]] { - guard let data = string.data(using: .utf8) else { - return [:] - } - guard let array = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [Any] else { - return [:] - } - var result: [SecureIdErrorKey: [String]] = [:] - for item in array { - guard let dict = item as? [String: Any] else { - continue - } - guard let type = dict["type"] as? String else { - continue - } - guard let text = dict["description"] as? String else { - continue - } - switch type { - case "personal_details": - if result[.personalDetails] == nil { - result[.personalDetails] = [] - } - result[.personalDetails]!.append(text) - break - default: - break - } - } - return result -} - -func filterSecureIdErrors(errors: [SecureIdErrorKey: [String]], afterSaving values: [SecureIdValueWithContext]) -> [SecureIdErrorKey: [String]] { - var result = errors - for value in values { - switch value.value.key { - case .personalDetails: - result.removeValue(forKey: .personalDetails) - default: - break - } - } - return errors -} diff --git a/TelegramUI/SecureIdValueFormFileItem.swift b/TelegramUI/SecureIdValueFormFileItem.swift index 262bf2ab1c..a9887244b2 100644 --- a/TelegramUI/SecureIdValueFormFileItem.swift +++ b/TelegramUI/SecureIdValueFormFileItem.swift @@ -3,20 +3,31 @@ import AsyncDisplayKit import Display import TelegramCore -private let textFont = Font.regular(17.0) +private let textFont = Font.regular(16.0) +private let labelFont = Font.regular(13.0) + +enum SecureIdValueFormFileItemLabel { + case timestamp + case error(String) + case text(String) +} final class SecureIdValueFormFileItem: FormControllerItem { let account: Account let context: SecureIdAccessContext - let document: SecureIdVerificationDocument + let document: SecureIdVerificationDocument? + let placeholder: UIImage? let title: String + let label: SecureIdValueFormFileItemLabel let activated: () -> Void - init(account: Account, context: SecureIdAccessContext, document: SecureIdVerificationDocument, title: String, activated: @escaping () -> Void) { + init(account: Account, context: SecureIdAccessContext, document: SecureIdVerificationDocument?, placeholder: UIImage?, title: String, label: SecureIdValueFormFileItemLabel, activated: @escaping () -> Void) { self.account = account self.context = context self.document = document + self.placeholder = placeholder self.title = title + self.label = label self.activated = activated } @@ -37,7 +48,9 @@ final class SecureIdValueFormFileItem: FormControllerItem { final class SecureIdValueFormFileItemNode: FormBlockItemNode { private let titleNode: ImmediateTextNode + private let labelNode: ImmediateTextNode let imageNode: TransformImageNode + private let placeholderNode: ASImageNode private let statusNode: RadialStatusNode private(set) var item: SecureIdValueFormFileItem? @@ -48,35 +61,61 @@ final class SecureIdValueFormFileItemNode: FormBlockItemNode (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) { var resourceUpdated = false if let previousItem = self.item { - resourceUpdated = !previousItem.document.resource.isEqual(to: item.document.resource) + if let previousDocument = previousItem.document, let document = item.document { + resourceUpdated = !previousDocument.resource.isEqual(to: document.resource) + } else if (previousItem.document != nil) != (item.document != nil) { + resourceUpdated = true + } } else { resourceUpdated = true } self.item = item + self.placeholderNode.image = item.placeholder + var progress: CGFloat? - switch item.document { - case .remote: - break - case let .local(local): - if case let .uploading(value) = local.state { - progress = CGFloat(value) - } + if let document = item.document { + switch document { + case .remote: + break + case let .local(local): + if case let .uploading(value) = local.state { + progress = CGFloat(value) + } + } + self.imageNode.isHidden = false + self.placeholderNode.isHidden = true + } else { + self.imageNode.isHidden = true + self.placeholderNode.isHidden = false } let progressState: RadialStatusNodeState @@ -91,20 +130,45 @@ final class SecureIdValueFormFileItemNode: FormBlockItemNode Bool { @@ -18,6 +19,9 @@ struct SecureIdVerificationLocalDocument: Equatable { if !lhs.resource.isEqual(to: rhs.resource) { return false } + if lhs.timestamp != rhs.timestamp { + return false + } if lhs.state != rhs.state { return false } @@ -43,6 +47,15 @@ enum SecureIdVerificationDocument: Equatable { } } + var timestamp: Int32 { + switch self { + case let .remote(file): + return file.timestamp + case let .local(file): + return file.timestamp + } + } + var resource: TelegramMediaResource { switch self { case let .remote(file): diff --git a/TelegramUI/SelectivePrivacySettingsPeersController.swift b/TelegramUI/SelectivePrivacySettingsPeersController.swift index 3897a75bc4..1c72adc186 100644 --- a/TelegramUI/SelectivePrivacySettingsPeersController.swift +++ b/TelegramUI/SelectivePrivacySettingsPeersController.swift @@ -207,10 +207,10 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini actionsDisposable.add(removePeerDisposable) let peersPromise = Promise<[Peer]>() - peersPromise.set(account.postbox.modify { modifier -> [Peer] in + peersPromise.set(account.postbox.transaction { transaction -> [Peer] in var result: [Peer] = [] for peerId in initialPeerIds { - if let peer = modifier.getPeer(peerId) { + if let peer = transaction.getPeer(peerId) { result.append(peer) } } @@ -250,11 +250,11 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini let applyPeers: Signal = peersPromise.get() |> take(1) |> mapToSignal { peers -> Signal<[Peer], NoError> in - return account.postbox.modify { modifier -> [Peer] in + return account.postbox.transaction { transaction -> [Peer] in var updatedPeers = peers var existingIds = Set(updatedPeers.map { $0.id }) for peerId in peerIds { - if let peer = modifier.getPeer(peerId), !existingIds.contains(peerId) { + if let peer = transaction.getPeer(peerId), !existingIds.contains(peerId) { existingIds.insert(peerId) updatedPeers.append(peer) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 662992ca39..9bcf13d9d7 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -17,6 +17,8 @@ private final class SettingsItemIcons { static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed() static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed() + static let secureId = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed() + static let support = UIImage(bundleImageName: "Settings/MenuIcons/Support")?.precomposed() static let faq = UIImage(bundleImageName: "Settings/MenuIcons/Faq")?.precomposed() } @@ -39,6 +41,7 @@ private struct SettingsItemArguments { let pushController: (ViewController) -> Void let presentController: (ViewController) -> Void let openLanguage: () -> Void + let openPassport: () -> Void let openSupport: () -> Void let openFaq: () -> Void let openEditing: () -> Void @@ -49,6 +52,7 @@ private enum SettingsSection: Int32 { case proxy case media case generalSettings + case passport case help } @@ -68,6 +72,7 @@ private enum SettingsEntry: ItemListNodeEntry { case dataAndStorage(PresentationTheme, UIImage?, String) case themes(PresentationTheme, UIImage?, String) case language(PresentationTheme, UIImage?, String, String) + case passport(PresentationTheme, UIImage?, String, String) case askAQuestion(PresentationTheme, UIImage?, String) case faq(PresentationTheme, UIImage?, String) @@ -82,6 +87,8 @@ private enum SettingsEntry: ItemListNodeEntry { return SettingsSection.media.rawValue case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: return SettingsSection.generalSettings.rawValue + case .passport: + return SettingsSection.passport.rawValue case .askAQuestion, .faq: return SettingsSection.help.rawValue } @@ -113,10 +120,12 @@ private enum SettingsEntry: ItemListNodeEntry { return 10 case .language: return 11 - case .askAQuestion: + case let .passport: return 12 - case .faq: + case .askAQuestion: return 13 + case .faq: + return 14 } } @@ -220,6 +229,12 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } + case let .passport(lhsTheme, lhsImage, lhsText, lhsValue): + if case let .passport(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .askAQuestion(lhsTheme, lhsImage, lhsText): if case let .askAQuestion(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true @@ -292,6 +307,10 @@ private enum SettingsEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openLanguage() }) + case let .passport(theme, image, text, value): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openPassport() + }) case let .askAQuestion(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSupport() @@ -337,7 +356,7 @@ private func settingsEntries(presentationData: PresentationData, state: Settings } if !proxySettings.servers.isEmpty { - entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, "Proxy", proxySettings.enabled ? "Enabled" : "Disabled")) + entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, presentationData.strings.Settings_Proxy, proxySettings.enabled ? presentationData.strings.UserInfo_NotificationsEnabled : presentationData.strings.Settings_ProxyDisabled)) } entries.append(.savedMessages(presentationData.theme, SettingsItemIcons.savedMessages, presentationData.strings.Settings_SavedMessages)) @@ -350,6 +369,10 @@ private func settingsEntries(presentationData: PresentationData, state: Settings entries.append(.themes(presentationData.theme, SettingsItemIcons.appearance, presentationData.strings.ChatSettings_Appearance.lowercased().capitalized)) entries.append(.language(presentationData.theme, SettingsItemIcons.language, presentationData.strings.Settings_AppLanguage, presentationData.strings.Localization_LanguageName)) + if GlobalExperimentalSettings.enablePassport { + entries.append(.passport(presentationData.theme, SettingsItemIcons.secureId, "Telegram Passport", "")) + } + entries.append(.askAQuestion(presentationData.theme, SettingsItemIcons.support, presentationData.strings.Settings_Support)) entries.append(.faq(presentationData.theme, SettingsItemIcons.faq, presentationData.strings.Settings_FAQ)) } @@ -454,6 +477,9 @@ public func settingsController(account: Account, accountManager: AccountManager) }, openLanguage: { let controller = LanguageSelectionController(account: account) presentControllerImpl?(controller, nil) + }, openPassport: { + let controller = SecureIdAuthController(account: account, mode: .list) + presentControllerImpl?(controller, nil) }, openSupport: { let supportPeer = Promise() supportPeer.set(supportPeerId(account: account)) @@ -473,8 +499,8 @@ public func settingsController(account: Account, accountManager: AccountManager) }, openFaq: { openFaq() }, openEditing: { - let _ = (account.postbox.modify { modifier -> (Peer?, CachedPeerData?) in - return (modifier.getPeer(account.peerId), modifier.getPeerCachedData(peerId: account.peerId)) + let _ = (account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in + return (transaction.getPeer(account.peerId), transaction.getPeerCachedData(peerId: account.peerId)) } |> deliverOnMainQueue).start(next: { peer, cachedData in if let peer = peer as? TelegramUser, let cachedData = cachedData as? CachedUserData { pushControllerImpl?(editSettingsController(account: account, currentName: .personName(firstName: peer.firstName ?? "", lastName: peer.lastName ?? ""), currentBioText: cachedData.about ?? "", accountManager: accountManager)) @@ -483,8 +509,8 @@ public func settingsController(account: Account, accountManager: AccountManager) }) changeProfilePhotoImpl = { - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(account.peerId) + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(account.peerId) } |> deliverOnMainQueue).start(next: { peer in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 01da75da2c..59dd2e9063 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -19,6 +19,8 @@ public enum ShareControllerSubject { case url(String) case text(String) case messages([Message]) + case image([TelegramMediaImageRepresentation]) + case mapMedia(TelegramMediaMap) case fromExternal(([PeerId], String) -> Signal) } @@ -184,11 +186,19 @@ public final class ShareController: ViewController { }) case .text: break + case .mapMedia: + break + case let .image(representations): + if saveToCameraRoll { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self?.saveToCameraRoll(image: representations) + }) + } case let .messages(messages): if messages.count == 1, let message = messages.first { if saveToCameraRoll { self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in - self?.saveToCameraRoll(message) + self?.saveToCameraRoll(message: message) }) } else if let showInChat = showInChat { self.defaultAction = ShareControllerAction(title: self.presentationData.strings.SharedMedia_ViewInChat, action: { [weak self] in @@ -269,6 +279,26 @@ public final class ShareController: ViewController { let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() + case let .image(representations): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + } + messages.append(.message(text: "", attributes: [], media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil), replyToMessageId: nil, localGroupingKey: nil)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + return .complete() + case let .mapMedia(media): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + } + messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: nil)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + return .complete() case let .messages(messages): for peerId in peerIds { var messagesToEnqueue: [EnqueueMessage] = [] @@ -298,6 +328,12 @@ public final class ShareController: ViewController { collectableItems.append(CollectableExternalShareItem(url: text, text: "", media: nil)) case let .text(string): collectableItems.append(CollectableExternalShareItem(url: "", text: string, media: nil)) + case let .image(representations): + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil) + collectableItems.append(CollectableExternalShareItem(url: "", text: "", media: media)) + case let .mapMedia(media): + let latLong = "\(media.latitude),\(media.longitude)" + collectableItems.append(CollectableExternalShareItem(url: "https://maps.apple.com/maps?ll=\(latLong)&q=\(latLong)&t=m", text: "", media: nil)) case let .messages(messages): for message in messages { var url: String? @@ -399,9 +435,14 @@ public final class ShareController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } - private func saveToCameraRoll(_ message: Message) { + private func saveToCameraRoll(message: Message) { if let media = message.media.first { self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(postbox: self.account.postbox, media: media)) } } + + private func saveToCameraRoll(image: [TelegramMediaImageRepresentation]) { + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image, reference: nil) + self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(postbox: self.account.postbox, media: media)) + } } diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index 0c3f77d19e..3155d23fba 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -13,15 +13,21 @@ final class StickerPackPreviewController: ViewController { private var animatedIn = false private let account: Account + private weak var parentNavigationController: NavigationController? + private var presentationData: PresentationData private let stickerPack: StickerPackReference + private var stickerPackContentsValue: LoadedStickerPack? + private let stickerPackDisposable = MetaDisposable() private let stickerPackContents = Promise() private let stickerPackInstalledDisposable = MetaDisposable() private let stickerPackInstalled = Promise() + private let openMentionDisposable = MetaDisposable() + var sendSticker: ((TelegramMediaFile) -> Void)? { didSet { if self.isNodeLoaded { @@ -37,8 +43,10 @@ final class StickerPackPreviewController: ViewController { } } - init(account: Account, stickerPack: StickerPackReference) { + init(account: Account, stickerPack: StickerPackReference, parentNavigationController: NavigationController?) { self.account = account + self.parentNavigationController = parentNavigationController + self.stickerPack = stickerPack self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -57,10 +65,46 @@ final class StickerPackPreviewController: ViewController { deinit { self.stickerPackDisposable.dispose() self.stickerPackInstalledDisposable.dispose() + self.openMentionDisposable.dispose() } override func loadDisplayNode() { - self.displayNode = StickerPackPreviewControllerNode(account: self.account) + self.displayNode = StickerPackPreviewControllerNode(account: self.account, openShare: { [weak self] in + guard let strongSelf = self else { + return + } + + if let stickerPackContentsValue = strongSelf.stickerPackContentsValue, case let .result(info, _, _) = stickerPackContentsValue, !info.shortName.isEmpty { + strongSelf.present(ShareController(account: strongSelf.account, subject: .url("https://t.me/addstickers/\(info.shortName)"), externalShare: true), in: .window(.root)) + strongSelf.dismiss() + } + }, openMention: { [weak self] mention in + guard let strongSelf = self else { + return + } + + let account = strongSelf.account + strongSelf.openMentionDisposable.set((resolvePeerByName(account: strongSelf.account, name: mention) + |> mapToSignal { peerId -> Signal in + if let peerId = peerId { + return account.postbox.loadedPeerWithId(peerId) + |> map(Optional.init) + } else { + return .single(nil) + } + } + |> deliverOnMainQueue).start(next: { peer in + guard let strongSelf = self else { + return + } + if let peer = peer { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + strongSelf.dismiss() + strongSelf.parentNavigationController?.pushViewController(infoController) + } + } + })) + }) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } @@ -84,6 +128,7 @@ final class StickerPackPreviewController: ViewController { strongSelf.dismiss() } else { strongSelf.controllerNode.updateStickerPack(next) + strongSelf.stickerPackContentsValue = next } } })) diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 0fc14b5751..ba90e5fc8b 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -7,6 +7,7 @@ import TelegramCore final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account + private let openShare: () -> Void private let presentationData: PresentationData private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -21,7 +22,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private let contentGridNode: GridNode private let installActionButtonNode: ASButtonNode private let installActionSeparatorNode: ASDisplayNode - private let contentTitleNode: ASTextNode + private let contentTitleNode: ImmediateTextNode + private let contentShareButtonNode: HighlightableButtonNode private let contentSeparatorNode: ASDisplayNode private var activityIndicator: ActivityIndicator? @@ -43,8 +45,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var hapticFeedback: HapticFeedback? - init(account: Account) { + init(account: Account, openShare: @escaping () -> Void, openMention: @escaping (String) -> Void) { self.account = account + self.openShare = openShare self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let theme = self.presentationData.theme @@ -94,11 +97,16 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.installActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal) self.installActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) - self.contentTitleNode = ASTextNode() + self.contentTitleNode = ImmediateTextNode() + self.contentTitleNode.displaysAsynchronously = false + self.contentTitleNode.maximumNumberOfLines = 1 + + self.contentShareButtonNode = HighlightableButtonNode() + self.contentShareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.presentationData.theme.actionSheet.controlAccentColor), for: []) + self.contentShareButtonNode.isHidden = true self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true - self.contentSeparatorNode.displaysAsynchronously = false self.contentSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor self.installActionSeparatorNode = ASDisplayNode() @@ -139,11 +147,29 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentContainerNode.addSubnode(self.installActionSeparatorNode) self.contentContainerNode.addSubnode(self.installActionButtonNode) self.wrappingScrollNode.addSubnode(self.contentTitleNode) + self.wrappingScrollNode.addSubnode(self.contentShareButtonNode) self.wrappingScrollNode.addSubnode(self.contentSeparatorNode) self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } + + self.contentTitleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) + self.contentTitleNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention) + } else { + return nil + } + } + + self.contentTitleNode.tapAttributeAction = { attributes in + if let mention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String, mention.count > 1 { + openMention(String(mention[mention.index(after: mention.startIndex)...])) + } + } + + self.contentShareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) } override func didLoad() { @@ -152,30 +178,18 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol if #available(iOSApplicationExtension 11.0, *) { self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } - - /* - let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.previewGesture(_:))) - longTapRecognizer.tapActionAtPoint = { [weak self] location in - if let strongSelf = self, let _ = strongSelf.contentGridNode.itemNodeAtPoint(location) as? StickerPackPreviewGridItemNode { - return .waitForHold(timeout: 0.2, acceptTap: true) - } - return .fail - } - self.contentGridNode.view.addGestureRecognizer(longTapRecognizer) - */ - self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { - return strongSelf.account.postbox.modify { modifier -> Bool in - return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + return strongSelf.account.postbox.transaction { transaction -> Bool in + return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [PeekControllerMenuItem] = [] if strongSelf.sendSticker != nil { - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.sendSticker?(item.file) } @@ -266,7 +280,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } itemCount = items.count if !self.didSetItems { - self.contentTitleNode.attributedText = NSAttributedString(string: info.title, font: Font.medium(20.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + let entities = generateTextEntities(info.title, enabledTypes: [.mention]) + self.contentTitleNode.attributedText = stringWithAppliedEntities(info.title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: Font.medium(20.0), linkFont: Font.medium(20.0), boldFont: Font.medium(20.0), italicFont: Font.medium(20.0), fixedFont: Font.medium(20.0)) self.didSetItems = true animateIn = true @@ -277,12 +292,16 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } - let titleSize = self.contentTitleNode.measure(contentContainerFrame.size) + let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 54.0, height: CGFloat.greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize) let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY) self.contentTitleNode.frame = titleFrame transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y)) + let titleButtonSize = CGSize(width: 44.0, height: 44.0) + let shareButtonFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + contentContainerFrame.size.width - titleButtonSize.width - 4.0, y: titleFrame.minY - 13.0), size: titleButtonSize) + transition.updateFrame(node: self.contentShareButtonNode, frame: shareButtonFrame) + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) @@ -293,7 +312,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol let minimallyRevealedRowCount: CGFloat = 3.5 let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) - let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight) + let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - buttonHeight) let bottomGridInset = buttonHeight transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) @@ -313,6 +332,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) if animateIn { + self.contentShareButtonNode.isHidden = false + self.contentShareButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + var durationOffset = 0.0 self.contentGridNode.forEachRow { itemNodes in for itemNode in itemNodes { @@ -378,6 +400,10 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + 15.0), size: titleSize) transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + let titleButtonSize = CGSize(width: 44.0, height: 44.0) + let shareButtonFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentFrame.size.width - titleButtonSize.width - 4.0, y: titleFrame.minY - 13.0), size: titleButtonSize) + transition.updateFrame(node: self.contentShareButtonNode, frame: shareButtonFrame) + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel))) if !compactFrame && CGFloat(0.0).isLessThanOrEqualTo(presentationLayout.contentOffset.y) { @@ -533,4 +559,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } } + + @objc private func sharePressed() { + self.openShare() + } } diff --git a/TelegramUI/StickerPaneSearchContainerNode.swift b/TelegramUI/StickerPaneSearchContainerNode.swift index fd8c363580..15ac9c56aa 100644 --- a/TelegramUI/StickerPaneSearchContainerNode.swift +++ b/TelegramUI/StickerPaneSearchContainerNode.swift @@ -12,11 +12,13 @@ final class StickerPaneSearchInteraction { let open: (StickerPackCollectionInfo) -> Void let install: (StickerPackCollectionInfo) -> Void let sendSticker: (TelegramMediaFile) -> Void + let getItemIsPreviewed: (StickerPackItem) -> Bool - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void) { + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.open = open self.install = install self.sendSticker = sendSticker + self.getItemIsPreviewed = getItemIsPreviewed } } @@ -97,6 +99,8 @@ private enum StickerSearchEntry: Identifiable, Comparable { interaction.open(info) }, install: { interaction.install(info) + }, getItemIsPreviewed: { item in + return interaction.getItemIsPreviewed(item) }) } } @@ -157,7 +161,9 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor - self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction) + self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in + return inputNodeInteraction?.previewedStickerPackItem == .pack(item) + }) self.searchBar = StickerPaneSearchBarNode(theme: theme, strings: strings) @@ -186,7 +192,7 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { let interaction = StickerPaneSearchInteraction(open: { [weak self] info in if let strongSelf = self { - strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }, install: { [weak self] info in if let strongSelf = self { @@ -211,25 +217,28 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { if let strongSelf = self { strongSelf.controllerInteraction.sendSticker(file) } + }, getItemIsPreviewed: { item in + return inputNodeInteraction.previewedStickerPackItem == .pack(item) }) let queue = Queue() let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil) + let currentRemotePacks = Atomic(value: nil) self.searchBar.textUpdated = { [weak self] text in guard let strongSelf = self else { return } - let signal: Signal<([(String, FoundStickerItem)], FoundStickerSets, Bool)?, NoError> + let signal: Signal<([(String, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> if !text.isEmpty { let stickers: Signal<[(String, FoundStickerItem)], NoError> = Signal { subscriber in var signals: [Signal<(String, [FoundStickerItem]), NoError>] = [] for entry in TGEmojiSuggestions.suggestions(forQuery: text.lowercased()) { if let entry = entry as? TGAlphacodeEntry { signals.append(searchStickers(account: account, query: entry.emoji) - |> take(1) - |> map { (entry.emoji, $0) }) + |> take(1) + |> map { (entry.emoji, $0) }) } } @@ -249,15 +258,19 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { let local = searchStickerSets(postbox: account.postbox, query: text) let remote = searchStickerSetsRemotely(network: account.network, query: text) let packs = local - |> mapToSignal { result -> Signal<(FoundStickerSets, Bool), NoError> in - return .single((result, false)) - |> then(remote |> map { remote -> (FoundStickerSets, Bool) in - return (result.merge(with: remote), true) + |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in + var localResult = result + if let currentRemote = currentRemotePacks.with ({ $0 }) { + localResult = localResult.merge(with: currentRemote) + } + return .single((localResult, false, nil)) + |> then(remote |> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in + return (result.merge(with: remote), true, remote) }) } signal = combineLatest(stickers, packs) - |> map { stickers, packs -> ([(String, FoundStickerItem)], FoundStickerSets, Bool)? in - return (stickers, packs.0, packs.1) + |> map { stickers, packs -> ([(String, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in + return (stickers, packs.0, packs.1, packs.2) } strongSelf.searchBar.activity = true } else { @@ -273,7 +286,10 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { } var entries: [StickerSearchEntry] = [] - if let (stickers, packs, final) = result { + if let (stickers, packs, final, remote) = result { + if let remote = remote { + let _ = currentRemotePacks.swap(remote) + } strongSelf.gridNode.isHidden = false strongSelf.trendingPane.isHidden = true @@ -301,6 +317,7 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { } } } else { + let _ = currentRemotePacks.swap(nil) strongSelf.searchBar.activity = false strongSelf.gridNode.isHidden = true strongSelf.trendingPane.isHidden = false @@ -400,13 +417,28 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? StickerPaneSearchStickerItemNode { itemNode.updatePreviewing(animated: animated) + } else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { + itemNode.updatePreviewing(animated: animated) } } + self.trendingPane.updatePreviewing(animated: animated) } - func itemAt(point: CGPoint) -> (ASDisplayNode, FoundStickerItem)? { - if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem { - return (itemNode, stickerItem) + func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPreviewPeekItem)? { + if !self.trendingPane.isHidden { + if let (itemNode, item) = self.trendingPane.itemAt(point: self.view.convert(point, to: self.trendingPane.view)) { + return (itemNode, .pack(item)) + } + } else { + if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) { + if let itemNode = itemNode as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem { + return (itemNode, .found(stickerItem)) + } else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { + if let (node, item) = itemNode.itemAt(point: self.view.convert(point, to: itemNode.view)) { + return (node, .pack(item)) + } + } + } } return nil } diff --git a/TelegramUI/StickerPaneSearchGlobaltem.swift b/TelegramUI/StickerPaneSearchGlobaltem.swift index 6bfeab5a73..4cd680ab06 100644 --- a/TelegramUI/StickerPaneSearchGlobaltem.swift +++ b/TelegramUI/StickerPaneSearchGlobaltem.swift @@ -15,11 +15,12 @@ final class StickerPaneSearchGlobalItem: GridItem { let unread: Bool let open: () -> Void let install: () -> Void + let getItemIsPreviewed: (StickerPackItem) -> Bool let section: GridSection? = nil let fillsRowWithHeight: CGFloat? = 128.0 - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.account = account self.theme = theme self.strings = strings @@ -29,6 +30,7 @@ final class StickerPaneSearchGlobalItem: GridItem { self.unread = unread self.open = open self.install = install + self.getItemIsPreviewed = getItemIsPreviewed } func node(layout: GridNodeLayout) -> GridItemNode { @@ -53,6 +55,25 @@ private let buttonFont = Font.medium(13.0) private final class TrendingTopItemNode: TransformImageNode { var file: TelegramMediaFile? = nil let loadDisposable = MetaDisposable() + + var currentIsPreviewing = false + + func updatePreviewing(animated: Bool, isPreviewing: Bool) { + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4, removeOnCompletion: false) + } + } else { + self.layer.removeAnimation(forKey: "transform.scale") + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + } + } + } } class StickerPaneSearchGlobalItemNode: GridItemNode { @@ -139,6 +160,8 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { func setup(item: StickerPaneSearchGlobalItem) { self.item = item self.setNeedsLayout() + + self.updatePreviewing(animated: false) } override func layout() { @@ -269,4 +292,33 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } } + + func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + guard let item = self.item else { + return nil + } + var index = 0 + for itemNode in self.itemNodes { + if itemNode.frame.contains(point), index < item.topItems.count { + return (itemNode, item.topItems[index]) + } + index += 1 + } + return nil + } + + func updatePreviewing(animated: Bool) { + guard let item = self.item else { + return + } + + var index = 0 + for itemNode in self.itemNodes { + if index < item.topItems.count { + let isPreviewing = item.getItemIsPreviewed(item.topItems[index]) + itemNode.updatePreviewing(animated: animated, isPreviewing: isPreviewing) + } + index += 1 + } + } } diff --git a/TelegramUI/StoredMessageFromSearchPeer.swift b/TelegramUI/StoredMessageFromSearchPeer.swift index 6a436b2f83..f66049a2f2 100644 --- a/TelegramUI/StoredMessageFromSearchPeer.swift +++ b/TelegramUI/StoredMessageFromSearchPeer.swift @@ -4,9 +4,9 @@ import TelegramCore import SwiftSignalKit func storedMessageFromSearchPeer(account: Account, peer: Peer) -> Signal { - return account.postbox.modify { modifier -> Void in - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in + return account.postbox.transaction { transaction -> Void in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in return updatedPeer }) } @@ -14,11 +14,11 @@ func storedMessageFromSearchPeer(account: Account, peer: Peer) -> Signal Signal { - return account.postbox.modify { modifier -> Void in - if modifier.getMessage(message.id) == nil { + return account.postbox.transaction { transaction -> Void in + if transaction.getMessage(message.id) == nil { for (_, peer) in message.peers { - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in return updatedPeer }) } @@ -26,7 +26,7 @@ func storedMessageFromSearch(account: Account, message: Message) -> Signal private let _presentationData = Promise() public var presentationData: Signal { @@ -80,7 +82,13 @@ public final class TelegramApplicationContext { } private var hasOngoingCallDisposable: Disposable? - public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, currentInAppNotificationSettings: InAppNotificationSettings, currentMediaInputSettings: MediaInputSettings, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { + private var immediateExperimentalUISettingsValue = Atomic(value: ExperimentalUISettings.defaultSettings) + public var immediateExperimentalUISettings: ExperimentalUISettings { + return self.immediateExperimentalUISettingsValue.with { $0 } + } + private var experimentalUISettingsDisposable: Disposable? + + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, account: Account?, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, postbox: Postbox) { self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground) if applicationBindings.isMainApp { @@ -88,20 +96,27 @@ public final class TelegramApplicationContext { } else { self.locationManager = nil } - if let stateManager = stateManager, let accountPeerId = accountPeerId, let viewTracker = viewTracker, let locationManager = self.locationManager { - self.liveLocationManager = LiveLocationManager(postbox: postbox, network: network, accountPeerId: accountPeerId, viewTracker: viewTracker, stateManager: stateManager, locationManager: locationManager, inForeground: applicationBindings.applicationInForeground) + if let account = account, let locationManager = self.locationManager { + self.liveLocationManager = LiveLocationManager(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, viewTracker: account.viewTracker, stateManager: account.stateManager, locationManager: locationManager, inForeground: applicationBindings.applicationInForeground) } else { self.liveLocationManager = nil } self.applicationBindings = applicationBindings self.accountManager = accountManager self.fetchManager = FetchManager(postbox: postbox) - self.currentPresentationData = Atomic(value: currentPresentationData) - self.currentAutomaticMediaDownloadSettings = Atomic(value: currentMediaDownloadSettings) - self.currentMediaInputSettings = Atomic(value: currentMediaInputSettings) - self._presentationData.set(.single(currentPresentationData) |> then(presentationData)) - self._automaticMediaDownloadSettings.set(.single(currentMediaDownloadSettings) |> then(automaticMediaDownloadSettings)) - self.currentInAppNotificationSettings = Atomic(value: currentInAppNotificationSettings) + self.currentPresentationData = Atomic(value: initialPresentationDataAndSettings.presentationData) + self.currentAutomaticMediaDownloadSettings = Atomic(value: initialPresentationDataAndSettings.automaticMediaDownloadSettings) + self.currentMediaInputSettings = Atomic(value: initialPresentationDataAndSettings.mediaInputSettings) + + if let account = account { + self._presentationData.set(.single(initialPresentationDataAndSettings.presentationData) |> then(updatedPresentationData(postbox: account.postbox))) + self._automaticMediaDownloadSettings.set(.single(initialPresentationDataAndSettings.automaticMediaDownloadSettings) |> then(updatedAutomaticMediaDownloadSettings(postbox: account.postbox))) + } else { + self._presentationData.set(.single(initialPresentationDataAndSettings.presentationData)) + self._automaticMediaDownloadSettings.set(.single(initialPresentationDataAndSettings.automaticMediaDownloadSettings)) + } + + self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) let inAppPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings])) @@ -158,6 +173,15 @@ public final class TelegramApplicationContext { self.hasOngoingCallDisposable = self.hasOngoingCall?.start(next: { value in let _ = immediateHasOngoingCallValue.swap(value) }) + + let immediateExperimentalUISettingsValue = self.immediateExperimentalUISettingsValue + let _ = immediateExperimentalUISettingsValue.swap(initialPresentationDataAndSettings.experimentalUISettings) + self.experimentalUISettingsDisposable = (postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.experimentalUISettings]) + |> deliverOnMainQueue).start(next: { view in + if let settings = view.values[ApplicationSpecificPreferencesKeys.experimentalUISettings] as? ExperimentalUISettings { + let _ = immediateExperimentalUISettingsValue.swap(settings) + } + }) } deinit { diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index a9058cb316..2936ed17c9 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -12,8 +12,8 @@ enum LocationBroadcastPanelSource { private func presentLiveLocationController(account: Account, peerId: PeerId, controller: ViewController) { if let id = account.telegramApplicationContext.liveLocationManager?.internalMessageForPeerId(peerId) { - let _ = (account.postbox.modify { modifier -> Message? in - return modifier.getMessage(id) + let _ = (account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(id) } |> deliverOnMainQueue).start(next: { [weak controller] message in if let message = message, let strongController = controller { let _ = openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, dismissInput: { diff --git a/TelegramUI/TermsOfServiceController.swift b/TelegramUI/TermsOfServiceController.swift new file mode 100644 index 0000000000..45966ade95 --- /dev/null +++ b/TelegramUI/TermsOfServiceController.swift @@ -0,0 +1,127 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import AsyncDisplayKit + +public class TermsOfServiceController: ViewController { + private var controllerNode: TermsOfServiceControllerNode { + return self.displayNode as! TermsOfServiceControllerNode + } + + private let theme: PresentationTheme + private let strings: PresentationStrings + private let text: String + private let entities: [MessageTextEntity] + private let ageConfirmation: Int32? + private let signingUp: Bool + private let accept: () -> Void + private let decline: () -> Void + private let openUrl: (String) -> Void + + private var didPlayPresentationAnimation = false + + public var inProgress: Bool = false { + didSet { + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = nil + } + self.controllerNode.inProgress = self.inProgress + } + } + + public init(theme: PresentationTheme, strings: PresentationStrings, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping () -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) { + self.theme = theme + self.strings = strings + self.text = text + self.entities = entities + self.ageConfirmation = ageConfirmation + self.signingUp = signingUp + self.accept = accept + self.decline = decline + self.openUrl = openUrl + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme), strings: NavigationBarStrings(back: strings.Common_Back, close: strings.Common_Close))) + + self.statusBar.statusBarStyle = self.theme.rootController.statusBar.style.style + + self.title = self.strings.TermsOfService_Title + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + self?.controllerNode.scrollToTop() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = TermsOfServiceControllerNode(theme: self.theme, strings: self.strings, text: self.text, entities: self.entities, ageConfirmation: self.ageConfirmation, leftAction: { [weak self] in + guard let strongSelf = self else { + return + } + + let text: String + let declineTitle: String + if strongSelf.signingUp { + text = strongSelf.strings.TermsOfService_DeclineUnauthorized + declineTitle = strongSelf.strings.TermsOfService_Decline + } else { + text = strongSelf.strings.TermsOfService_DeclineAuthorized + declineTitle = strongSelf.strings.TermsOfService_DeclineAndDelete + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: strongSelf.strings.TermsOfService_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { + self?.decline() + }), TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: { + })], actionLayout: .vertical), in: .window(.root)) + }, rightAction: { [weak self] in + guard let strongSelf = self else { + return + } + + if let ageConfirmation = strongSelf.ageConfirmation { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: strongSelf.strings.TermsOfService_AgeVerificationTitle, text: strongSelf.strings.TermsOfService_AgeVerificationText(Int(ageConfirmation)).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.strings.TermsOfService_Confirm, action: { + self?.accept() + })]), in: .window(.root)) + } else { + strongSelf.accept() + } + }, openUrl: self.openUrl, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } +} diff --git a/TelegramUI/TermsOfServiceControllerNode.swift b/TelegramUI/TermsOfServiceControllerNode.swift new file mode 100644 index 0000000000..395a1d4cda --- /dev/null +++ b/TelegramUI/TermsOfServiceControllerNode.swift @@ -0,0 +1,250 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import AsyncDisplayKit + +final class TermsOfServiceControllerNode: ViewControllerTracingNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let text: String + private let entities: [MessageTextEntity] + private let ageConfirmation: Int32? + private let leftAction: () -> Void + private let rightAction: () -> Void + private let openUrl: (String) -> Void + private let present: (ViewController, Any?) -> Void + + private let scrollNode: ASScrollNode + private let contentBackgroundNode: ASDisplayNode + private let contentTextNode: ImmediateTextNode + private let toolbarNode: ASDisplayNode + private let toolbarSeparatorNode: ASDisplayNode + private let leftActionNode: HighlightableButtonNode + private let leftActionTextNode: ImmediateTextNode + private let rightActionNode: HighlightableButtonNode + private let rightActionTextNode: ImmediateTextNode + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + var inProgress: Bool = false { + didSet { + if self.inProgress != oldValue { + self.leftActionTextNode.alpha = self.inProgress ? 0.5 : 1.0 + self.rightActionTextNode.alpha = self.inProgress ? 0.5 : 1.0 + self.leftActionNode.isEnabled = !self.inProgress + self.rightActionNode.isEnabled = !self.inProgress + } + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, leftAction: @escaping () -> Void, rightAction: @escaping () -> Void, openUrl: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.theme = theme + self.strings = strings + self.text = text + self.entities = entities + self.ageConfirmation = ageConfirmation + self.leftAction = leftAction + self.rightAction = rightAction + self.openUrl = openUrl + self.present = present + + self.scrollNode = ASScrollNode() + self.contentBackgroundNode = ASDisplayNode() + self.contentTextNode = ImmediateTextNode() + self.contentTextNode.displaysAsynchronously = false + self.contentTextNode.maximumNumberOfLines = 0 + self.contentTextNode.attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: theme.list.itemPrimaryTextColor, linkColor: theme.list.itemAccentColor, baseFont: Font.regular(15.0), linkFont: Font.regular(15.0), boldFont: Font.semibold(15.0), italicFont: Font.italic(15.0), fixedFont: Font.monospace(15.0)) + + self.toolbarNode = ASDisplayNode() + self.toolbarSeparatorNode = ASDisplayNode() + self.leftActionNode = HighlightableButtonNode() + self.leftActionTextNode = ImmediateTextNode() + self.leftActionTextNode.displaysAsynchronously = false + self.leftActionTextNode.isLayerBacked = true + self.leftActionTextNode.attributedText = NSAttributedString(string: self.strings.TermsOfService_Disagree, font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor) + self.rightActionNode = HighlightableButtonNode() + self.rightActionTextNode = ImmediateTextNode() + self.rightActionTextNode.displaysAsynchronously = false + self.rightActionTextNode.isLayerBacked = true + self.rightActionTextNode.attributedText = NSAttributedString(string: self.strings.TermsOfService_Agree, font: Font.semibold(17.0), textColor: self.theme.list.itemAccentColor) + + super.init() + + self.backgroundColor = self.theme.list.blocksBackgroundColor + self.toolbarNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor + + self.contentBackgroundNode.backgroundColor = self.theme.list.itemBlocksBackgroundColor + + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.contentBackgroundNode) + self.scrollNode.addSubnode(self.contentTextNode) + self.addSubnode(self.toolbarNode) + self.addSubnode(self.toolbarSeparatorNode) + self.addSubnode(self.leftActionTextNode) + self.addSubnode(self.leftActionNode) + self.addSubnode(self.rightActionTextNode) + self.addSubnode(self.rightActionNode) + + self.leftActionNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.leftActionTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.leftActionTextNode.alpha = 0.4 + } else { + strongSelf.leftActionTextNode.alpha = 1.0 + strongSelf.leftActionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + self.leftActionNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.rightActionTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.rightActionTextNode.alpha = 0.4 + } else { + strongSelf.rightActionTextNode.alpha = 1.0 + strongSelf.rightActionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + self.leftActionNode.addTarget(self, action: #selector(self.leftActionPressed), forControlEvents: .touchUpInside) + self.rightActionNode.addTarget(self, action: #selector(self.rightActionPressed), forControlEvents: .touchUpInside) + + self.contentTextNode.linkHighlightColor = self.theme.list.itemAccentColor.withAlphaComponent(0.5) + self.contentTextNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.Url) + } else if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.Url) + } else if let _ = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] { + return NSAttributedStringKey(rawValue: TelegramTextAttributes.Url) + } else { + return nil + } + } + self.contentTextNode.tapAttributeAction = { [weak self] attributes in + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { + self?.openUrl(url) + } + } + self.contentTextNode.longTapAttributeAction = { [weak self] attributes in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + self?.openUrl(url) + }), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, nil) + } else if let mention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention.mention), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention.mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, nil) + } else if let mention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention), + ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, nil) + } + } + } + + deinit { + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: []) + insets.top += navigationBarHeight + + let toolbarHeight: CGFloat = 44.0 + insets.bottom += layout.safeInsets.bottom + + let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - toolbarHeight), size: CGSize(width: layout.size.width, height: insets.bottom + toolbarHeight)) + + insets.bottom += toolbarHeight + + transition.updateFrame(node: self.toolbarNode, frame: toolbarFrame) + transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarFrame.minY), size: CGSize(width: toolbarFrame.width, height: UIScreenPixel))) + + let leftActionSize = self.leftActionTextNode.updateLayout(CGSize(width: floor(layout.size.width / 2.0), height: CGFloat.greatestFiniteMagnitude)) + let rightActionSize = self.rightActionTextNode.updateLayout(CGSize(width: floor(layout.size.width / 2.0), height: CGFloat.greatestFiniteMagnitude)) + let leftActionTextFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 15.0, y: toolbarFrame.minY + floor((toolbarHeight - leftActionSize.height) / 2.0)), size: leftActionSize) + let rightActionTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - 15.0 - rightActionSize.width, y: toolbarFrame.minY + floor((toolbarHeight - rightActionSize.height) / 2.0)), size: rightActionSize) + transition.updateFrame(node: self.leftActionTextNode, frame: leftActionTextFrame) + transition.updateFrame(node: self.rightActionTextNode, frame: rightActionTextFrame) + self.leftActionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: toolbarFrame.minY), size: CGSize(width: leftActionTextFrame.maxX + 15.0, height: toolbarHeight)) + self.rightActionNode.frame = CGRect(origin: CGPoint(x: rightActionTextFrame.minX - 15.0, y: toolbarFrame.minY), size: CGSize(width: layout.size.width - (rightActionTextFrame.minX - 15.0), height: toolbarHeight)) + + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom)) + transition.updateFrame(node: self.scrollNode, frame: scrollFrame) + + let containerInset: CGFloat = 32.0 + let contentInsets = UIEdgeInsets(top: 15.0, left: 15.0 + layout.safeInsets.left, bottom: 15.0, right: 15.0 + layout.safeInsets.right) + let contentSize = self.contentTextNode.updateLayout(CGSize(width: layout.size.width - contentInsets.left, height: CGFloat.greatestFiniteMagnitude)) + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: layout.size.width, height: contentSize.height + contentInsets.top + contentInsets.bottom)) + self.contentTextNode.frame = CGRect(origin: CGPoint(x: contentFrame.minX + contentInsets.left, y: contentFrame.minY + contentInsets.top), size: contentSize) + self.contentBackgroundNode.frame = contentFrame + self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: containerInset + contentFrame.height + containerInset) + } + + func scrollToTop() { + self.scrollNode.view.scrollRectToVisible(CGRect(origin: CGPoint(), size: CGSize()), animated: true) + } + + 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?() + }) + } + + @objc private func leftActionPressed() { + self.leftAction() + } + + @objc private func rightActionPressed() { + self.rightAction() + } +} diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 4a2eb3c2e2..46e3a04e6d 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -88,7 +88,9 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentController: { _, _ in }, navigationController: { + return nil + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in @@ -143,8 +145,8 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { let chatPresentationData = ChatPresentationData(theme: item.theme, fontSize: item.fontSize, strings: item.strings, wallpaper: item.wallpaper, timeFormat: item.timeFormat) - let item2: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "Ahh you kids today with techno music! Enjoy the classics, like Hasselhoff!", attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none), disableDate: true) - let item1: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: TelegramUser(id: item.account.peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), text: "I can't take you seriously right now. Sorry..", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none), disableDate: true) + let item2: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "Ahh you kids today with techno music! Enjoy the classics, like Hasselhoff!", attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) + let item1: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: TelegramUser(id: item.account.peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), text: "I can't take you seriously right now. Sorry..", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) var node1: ListViewItemNode? if let current = currentNode1 { diff --git a/TelegramUI/ThrottledValue.swift b/TelegramUI/ThrottledValue.swift new file mode 100644 index 0000000000..a396bbb249 --- /dev/null +++ b/TelegramUI/ThrottledValue.swift @@ -0,0 +1,46 @@ +import Foundation +import UIKit +import SwiftSignalKit + +final class ThrottledValue { + private var value: T + private let interval: Double + private var previousSetTimestamp: Double + private let valuePromise: ValuePromise + private var timer: SwiftSignalKit.Timer? + + init(value: T, interval: Double) { + self.value = value + self.interval = interval + self.previousSetTimestamp = CACurrentMediaTime() + self.valuePromise = ValuePromise(value) + } + + deinit { + self.timer?.invalidate() + } + + func set(value: T) { + guard self.value != value else { + return + } + let timestamp = CACurrentMediaTime() + if timestamp > self.previousSetTimestamp + self.interval { + self.previousSetTimestamp = timestamp + self.valuePromise.set(value) + } else { + self.timer?.invalidate() + let timer = SwiftSignalKit.Timer(timeout: self.interval, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.valuePromise.set(strongSelf.value) + } + }, queue: Queue.mainQueue()) + self.timer = timer + timer.start() + } + } + + func get() -> Signal { + return self.valuePromise.get() + } +} diff --git a/TelegramUI/TwoStepVerificationResetController.swift b/TelegramUI/TwoStepVerificationResetController.swift index 33ad0daf72..4adf3053d5 100644 --- a/TelegramUI/TwoStepVerificationResetController.swift +++ b/TelegramUI/TwoStepVerificationResetController.swift @@ -157,7 +157,7 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, } } if let code = code { - resetPasswordDisposable.set((recoverTwoStepVerificationPassword(account: account, code: code) |> deliverOnMainQueue).start(error: { error in + resetPasswordDisposable.set((recoverTwoStepVerificationPassword(network: account.network, code: code) |> deliverOnMainQueue).start(error: { error in updateState { return $0.withUpdatedChecking(false) } diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index c383486e7d..07b481f92a 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -318,7 +318,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep updateState { $0.withUpdatedChecking(true) } - setupResultDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(account: account) |> deliverOnMainQueue).start(next: { emailPattern in + setupResultDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(network: account.network) |> deliverOnMainQueue).start(next: { emailPattern in updateState { $0.withUpdatedChecking(false) } diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index 0b547a6f4d..fd4189b21d 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -49,6 +49,24 @@ class UniversalVideoGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { + guard let contentInfo = self.contentInfo, case let .message(message) = contentInfo else { + return nil + } + if let id = message.groupInfo?.stableId { + var media: Media? + for m in message.media { + if let m = m as? TelegramMediaImage { + media = m + } else if let m = m as? TelegramMediaFile, m.isVideo { + media = m + } + } + if let media = media { + if let item = ChatMediaGalleryThumbnailItem(account: self.account, media: media) { + return (Int64(id), item) + } + } + } return nil } } diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index cda79d8052..6bec32b68f 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -2,6 +2,7 @@ import Foundation import SwiftSignalKit import Postbox import TelegramCore +import MtProtoKitDynamic private enum ParsedInternalPeerUrlParameter { case botStart(String) @@ -13,7 +14,7 @@ private enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case stickerPack(String) case join(String) - case proxy(host: String, port: Int32, username: String?, password: String?) + case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) } private enum ParsedUrl { @@ -29,7 +30,7 @@ enum ResolvedUrl { case channelMessage(peerId: PeerId, messageId: MessageId) case stickerPack(name: String) case instantView(TelegramMediaWebpage, String?) - case proxy(host: String, port: Int32, username: String?, password: String?) + case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) case join(String) } @@ -48,6 +49,7 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { var port: String? var user: String? var pass: String? + var secret: Data? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -59,13 +61,18 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { user = value } else if queryItem.name == "pass" { pass = value + } else if queryItem.name == "secret" { + let data = dataWithHexString(value) + if data.count == 16 || (data.count == 17 && MTSocksProxySettings.secretSupportsExtendedPadding(data)) { + secret = data + } } } } } if let server = server, !server.isEmpty, let port = port, let portValue = Int32(port) { - return .proxy(host: server, port: portValue, username: user, password: pass) + return .proxy(host: server, port: portValue, username: user, password: pass, secret: secret) } } else { for queryItem in queryItems { @@ -127,8 +134,8 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig return .single(.stickerPack(name: name)) case let .join(link): return .single(.join(link)) - case let .proxy(host, port, username, password): - return .single(.proxy(host: host, port: port, username: username, password: password)) + case let .proxy(host, port, username, password, secret): + return .single(.proxy(host: host, port: port, username: username, password: password, secret: secret)) } } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 9af34d8d56..7aeb955660 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -223,10 +223,10 @@ private enum UserInfoEntry: ItemListNodeEntry { switch self { case .info: return 0 - case .about: - return 1 case let .phoneNumber(_, index, _, _, _): - return 2 + index + return 1 + index + case .about: + return 999 case .userName: return 1000 case .sendMessage: @@ -423,20 +423,17 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: user.botInfo == nil)) - if let cachedUserData = view.cachedData as? CachedUserData { - if let about = cachedUserData.about, !about.isEmpty { - entries.append(UserInfoEntry.about(presentationData.theme, presentationData.strings.Profile_About, about)) - } - } if let phoneNumber = user.phone, !phoneNumber.isEmpty { let formattedNumber = formatPhoneNumber(phoneNumber) let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formattedNumber) - var existingNumbers = Set() - var index = 0 var found = false + + var existingNumbers = Set() + var phoneNumbers: [(DeviceContactPhoneNumber, Bool)] = [] + for contact in deviceContacts { inner: for number in contact.phoneNumbers { var isMain = false @@ -449,13 +446,24 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat found = true isMain = true } - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.number.normalized.rawValue, isMain)) - index += 1 + + phoneNumbers.append((number, isMain)) } } if !found { entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, "home", formattedNumber, false)) index += 1 + } else { + for (number, isMain) in phoneNumbers { + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.number.normalized.rawValue, isMain && phoneNumbers.count != 1)) + index += 1 + } + } + } + + if let cachedUserData = view.cachedData as? CachedUserData { + if let about = cachedUserData.about, !about.isEmpty { + entries.append(UserInfoEntry.about(presentationData.theme, presentationData.strings.Profile_About, about)) } } @@ -515,12 +523,12 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } private func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal { - return postbox.modify { modifier -> Peer? in - guard let peer = modifier.getPeer(peerId) else { + return postbox.transaction { transaction -> Peer? in + guard let peer = transaction.getPeer(peerId) else { return nil } if let peer = peer as? TelegramSecretChat { - return modifier.getPeer(peer.regularPeerId) + return transaction.getPeer(peer.regularPeerId) } else { return peer } @@ -579,8 +587,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll account.telegramApplicationContext.navigateToCurrentCall?() } else { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in - return (modifier.getPeer(peer.id), modifier.getPeer(currentPeerId)) + let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { peer, current in if let peer = peer, let current = current { presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), 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: { @@ -690,9 +698,9 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, changeNotificationSoundSettings: { - let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in - let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings - let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + let _ = (account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings return (peerSettings, globalSettings) } |> deliverOnMainQueue).start(next: { settings in let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.privateChats.sound, completion: { sound in @@ -774,9 +782,9 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, displayAboutContextMenu: { text in displayAboutContextMenuImpl?(text) }, openEncryptionKey: { fingerprint in - let _ = (account.postbox.modify { modifier -> Peer? in - if let peer = modifier.getPeer(peerId) as? TelegramSecretChat { - if let userPeer = modifier.getPeer(peer.regularPeerId) { + let _ = (account.postbox.transaction { transaction -> Peer? in + if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { + if let userPeer = transaction.getPeer(peer.regularPeerId) { return userPeer } } @@ -914,7 +922,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll if let peer = peer as? TelegramUser, let phone = peer.phone { let selectionController = PeerSelectionController(account: account) selectionController.peerSelected = { [weak selectionController] peerId in - let _ = (enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id), replyToMessageId: nil, localGroupingKey: nil)]) |> deliverOnMainQueue).start(completed: { + let _ = (enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil), replyToMessageId: nil, localGroupingKey: nil)]) |> deliverOnMainQueue).start(completed: { if let controller = controller { let ready = ValuePromise() let _ = (ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in @@ -929,14 +937,14 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) } startSecretChatImpl = { [weak controller] in - let _ = (account.postbox.modify { modifier -> PeerId? in - let filteredPeerIds = Array(modifier.getAssociatedPeerIds(peerId)).filter { $0.namespace == Namespaces.Peer.SecretChat } + let _ = (account.postbox.transaction { transaction -> PeerId? in + let filteredPeerIds = Array(transaction.getAssociatedPeerIds(peerId)).filter { $0.namespace == Namespaces.Peer.SecretChat } var activeIndices: [ChatListIndex] = [] for associatedId in filteredPeerIds { - if let state = (modifier.getPeer(associatedId) as? TelegramSecretChat)?.embeddedState { + if let state = (transaction.getPeer(associatedId) as? TelegramSecretChat)?.embeddedState { switch state { case .active, .handshake: - if let (_, index) = modifier.getPeerChatListIndex(associatedId) { + if let (_, index) = transaction.getPeerChatListIndex(associatedId) { activeIndices.append(index) } default: diff --git a/TelegramUI/UserInfoEditingPhoneItem.swift b/TelegramUI/UserInfoEditingPhoneItem.swift index 7346bdae13..a85f0e04c1 100644 --- a/TelegramUI/UserInfoEditingPhoneItem.swift +++ b/TelegramUI/UserInfoEditingPhoneItem.swift @@ -147,6 +147,15 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { } } + override func didLoad() { + super.didLoad() + + if let item = self.item { + self.phoneNode.numberField?.textField.textColor = item.theme.list.itemPrimaryTextColor + self.phoneNode.numberField?.textField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance + } + } + func asyncLayout() -> (_ item: UserInfoEditingPhoneItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) @@ -185,11 +194,14 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { strongSelf.item = item strongSelf.layoutParams = params - if let _ = updatedTheme { + if let updatedTheme = updatedTheme { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.labelSeparatorNode.backgroundColor = itemSeparatorColor + + strongSelf.phoneNode.numberField?.textField.textColor = updatedTheme.list.itemPrimaryTextColor + strongSelf.phoneNode.numberField?.textField.keyboardAppearance = updatedTheme.chatList.searchBarKeyboardColor.keyboardAppearance } let revealOffset = strongSelf.revealOffset @@ -229,7 +241,7 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) } }) } diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index ac5383dc5d..93b599c494 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -45,8 +45,8 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { } func updateVoiceCallSettingsSettingsInteractively(postbox: Postbox, _ f: @escaping (VoiceCallSettings) -> VoiceCallSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.voiceCallSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.voiceCallSettings, { entry in let currentSettings: VoiceCallSettings if let entry = entry as? VoiceCallSettings { currentSettings = entry diff --git a/TelegramUI/Wallpapers.swift b/TelegramUI/Wallpapers.swift deleted file mode 100644 index 662b18f518..0000000000 --- a/TelegramUI/Wallpapers.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation -import Postbox -import TelegramCore -import SwiftSignalKit - -public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { - case builtin - case color(Int32) - case image([TelegramMediaImageRepresentation]) - - public init(decoder: PostboxDecoder) { - switch decoder.decodeInt32ForKey("v", orElse: 0) { - case 0: - self = .builtin - case 1: - self = .color(decoder.decodeInt32ForKey("c", orElse: 0)) - case 2: - self = .image(decoder.decodeObjectArrayWithDecoderForKey("i")) - default: - assertionFailure() - self = .builtin - } - } - - public func encode(_ encoder: PostboxEncoder) { - switch self { - case .builtin: - encoder.encodeInt32(0, forKey: "v") - case let .color(color): - encoder.encodeInt32(1, forKey: "v") - encoder.encodeInt32(color, forKey: "c") - case let .image(representations): - encoder.encodeInt32(2, forKey: "v") - encoder.encodeObjectArray(representations, forKey: "i") - } - } - - public static func ==(lhs: TelegramWallpaper, rhs: TelegramWallpaper) -> Bool { - switch lhs { - case .builtin: - if case .builtin = rhs { - return true - } else { - return false - } - case let .color(color): - if case .color(color) = rhs { - return true - } else { - return false - } - case let .image(lhsRepresentations): - if case let .image(rhsRepresentations) = rhs, lhsRepresentations == rhsRepresentations { - return true - } else { - return false - } - } - } -} - -func telegramWallpapers(account: Account) -> Signal<[TelegramWallpaper], NoError> { - return account.postbox.modify { modifier -> [TelegramWallpaper] in - let items = modifier.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers) - if items.count == 0 { - return [.color(0x000000), .builtin] - } else { - return items.map { $0.contents as! TelegramWallpaper } - } - } |> mapToSignal { list -> Signal<[TelegramWallpaper], NoError> in - let remote = account.network.request(Api.functions.account.getWallPapers()) - |> retryRequest - |> mapToSignal { result -> Signal<[TelegramWallpaper], NoError> in - var items: [TelegramWallpaper] = [] - for item in result { - switch item { - case let .wallPaper(_, _, sizes, color): - items.append(.image(telegramMediaImageRepresentationsFromApiSizes(sizes))) - case let .wallPaperSolid(_, _, bgColor, color): - items.append(.color(bgColor)) - } - } - items.removeFirst() - items.insert(.color(0x000000), at: 0) - items.insert(.builtin, at: 1) - - if items == list { - return .complete() - } else { - return account.postbox.modify { modifier -> [TelegramWallpaper] in - var entries: [OrderedItemListEntry] = [] - for item in items { - var intValue = Int32(entries.count) - let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) - entries.append(OrderedItemListEntry(id: id, contents: item)) - } - modifier.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers, items: entries) - - return items - } - } - } - return .single(list) |> then(remote) - } -} diff --git a/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift index 07652f2875..4a81c5f2f6 100644 --- a/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift +++ b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift @@ -35,6 +35,8 @@ private final class LegacyLocationVenueIconTask: NSObject { } } +private let genericIconImage = TGComponentsImageNamed("LocationMessagePinIcon")?.precomposed() + final class LegacyLocationVenueIconDataSource: TGImageDataSource { private let account: () -> Account? @@ -67,10 +69,62 @@ final class LegacyLocationVenueIconDataSource: TGImageDataSource { return nil } + private static func unavailableImage(for uri: String) -> TGDataResource? { + let args: [AnyHashable : Any] + let argumentsString = String(uri[uri.index(uri.startIndex, offsetBy: "location-venue-icon://".count)...]) + args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! + + guard let width = Int((args["width"] as! String)), width > 1 else { + return nil + } + guard let height = Int((args["height"] as! String)), height > 1 else { + return nil + } + + guard let colorN = (args["color"] as? String).flatMap({ Int($0) }) else { + return nil + } + + let color = UIColor(rgb: UInt32(colorN)) + + let size = CGSize(width: CGFloat(width), height: CGFloat(height)) + + guard let iconSourceImage = genericIconImage.flatMap({ generateTintedImage(image: $0, color: color) }) else { + return nil + } + + UIGraphicsBeginImageContextWithOptions(iconSourceImage.size, false, iconSourceImage.scale) + var context = UIGraphicsGetCurrentContext()! + iconSourceImage.draw(at: CGPoint()) + context.setBlendMode(.sourceAtop) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: iconSourceImage.size)) + + let tintedIconImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + context = UIGraphicsGetCurrentContext()! + let fitSize = CGSize(width: size.width - 4.0 * 2.0, height: size.height - 4.0 * 2.0) + let imageSize = iconSourceImage.size.aspectFitted(fitSize) + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) + tintedIconImage?.draw(in: imageRect) + + let iconImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext() + + if let iconImage = iconImage { + sharedImageCache.setImage(iconImage, forKey: uri, attributes: nil) + return TGDataResource(image: iconImage, decoded: true) + } + + return nil + } + override func loadDataAsync(withUri uri: String!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> Any! { if let account = self.account() { let args: [AnyHashable : Any] - let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "location-venue-icon://".characters.count)) + let argumentsString = String(uri[uri.index(uri.startIndex, offsetBy: "location-venue-icon://".count)...]) args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! guard let width = Int((args["width"] as! String)), width > 1 else { @@ -80,10 +134,18 @@ final class LegacyLocationVenueIconDataSource: TGImageDataSource { return nil } - guard let url = args["url"] as? String else { + guard let colorN = (args["color"] as? String).flatMap({ Int($0) }) else { return nil } + guard let type = args["type"] as? String else { + return LegacyLocationVenueIconDataSource.unavailableImage(for: uri) + } + + let color = UIColor(rgb: UInt32(colorN)) + + let url = "https://ss3.4sqi.net/img/categories_v2/\(type)_88.png" + let size = CGSize(width: CGFloat(width), height: CGFloat(height)) return LegacyLocationVenueIconTask(account: account, url: url, completion: { data in @@ -92,7 +154,7 @@ final class LegacyLocationVenueIconDataSource: TGImageDataSource { var context = UIGraphicsGetCurrentContext()! iconSourceImage.draw(at: CGPoint()) context.setBlendMode(.sourceAtop) - context.setFillColor(UIColor(rgb: 0xa0a0a0).cgColor) + context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: iconSourceImage.size)) let tintedIconImage = UIGraphicsGetImageFromCurrentImageContext() @@ -100,9 +162,7 @@ final class LegacyLocationVenueIconDataSource: TGImageDataSource { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) context = UIGraphicsGetCurrentContext()! - context.setFillColor(UIColor(rgb: 0xf2f2f2).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - let imageRect = CGRect(x: 4.0, y: 4.0, width: 32.0, height: 32.0) + let imageRect = CGRect(x: 4.0, y: 4.0, width: size.width - 4.0 * 2.0, height: size.height - 4.0 * 2.0) tintedIconImage?.draw(in: imageRect) let iconImage = UIGraphicsGetImageFromCurrentImageContext(); @@ -112,6 +172,10 @@ final class LegacyLocationVenueIconDataSource: TGImageDataSource { sharedImageCache.setImage(iconImage, forKey: uri, attributes: nil) completion?(TGDataResource(image: iconImage, decoded: true)) } + } else { + if let image = LegacyLocationVenueIconDataSource.unavailableImage(for: uri) { + completion?(image) + } } }) } else {