no message

This commit is contained in:
Peter 2017-02-11 17:05:23 +03:00
parent d0a9b00844
commit 32efb5962d
240 changed files with 16763 additions and 497 deletions

View File

@ -42,6 +42,7 @@
D01B279D1E394A500022A4C0 /* NotificationsAndSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */; };
D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */; };
D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */; };
D01D6BFC1E42AB3C006151C6 /* EmojiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */; };
D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */; };
D0215D381E040F53001A0B1E /* InstantPageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D371E040F53001A0B1E /* InstantPageNode.swift */; };
D0215D3A1E041003001A0B1E /* InstantPageLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D391E041003001A0B1E /* InstantPageLayout.swift */; };
@ -91,7 +92,84 @@
D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; };
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; };
D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; };
D049EAE21E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */; };
D049EAE41E44949F00A2CD3A /* HorizontalStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */; };
D049EAE61E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */; };
D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */; };
D049EAF31E44DE2500A2CD3A /* AuthorizationSequenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */; };
D04B66B81DD672D00049C3D2 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B66B71DD672D00049C3D2 /* GeoLocation.swift */; };
D04BB2B31E44E56200650E93 /* AuthorizationSequenceSplashController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2B21E44E56200650E93 /* AuthorizationSequenceSplashController.swift */; };
D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2B41E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift */; };
D04BB2B91E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2B81E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift */; };
D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2BA1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift */; };
D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2BD1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift */; };
D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2BF1E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift */; };
D04BB2C31E45020A00650E93 /* AuthorizationSequencePasswordEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2C21E45020A00650E93 /* AuthorizationSequencePasswordEntryController.swift */; };
D04BB2C51E45022C00650E93 /* AuthorizationSequencePasswordEntryControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2C41E45022C00650E93 /* AuthorizationSequencePasswordEntryControllerNode.swift */; };
D04BB32B1E48797500650E93 /* linmath.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2CA1E48797500650E93 /* linmath.h */; };
D04BB32C1E48797500650E93 /* animations.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2CC1E48797500650E93 /* animations.c */; };
D04BB32D1E48797500650E93 /* animations.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2CD1E48797500650E93 /* animations.h */; };
D04BB32E1E48797500650E93 /* buffer.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2CE1E48797500650E93 /* buffer.c */; };
D04BB32F1E48797500650E93 /* buffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2CF1E48797500650E93 /* buffer.h */; };
D04BB3301E48797500650E93 /* config.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D01E48797500650E93 /* config.h */; };
D04BB3311E48797500650E93 /* macros.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D11E48797500650E93 /* macros.h */; };
D04BB3321E48797500650E93 /* math_helper.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D21E48797500650E93 /* math_helper.h */; };
D04BB3331E48797500650E93 /* matrix.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D31E48797500650E93 /* matrix.h */; };
D04BB3341E48797500650E93 /* objects.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2D41E48797500650E93 /* objects.c */; };
D04BB3351E48797500650E93 /* objects.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D51E48797500650E93 /* objects.h */; };
D04BB3361E48797500650E93 /* program.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2D61E48797500650E93 /* program.c */; };
D04BB3371E48797500650E93 /* program.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D71E48797500650E93 /* program.h */; };
D04BB3381E48797500650E93 /* rngs.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2D81E48797500650E93 /* rngs.c */; };
D04BB3391E48797500650E93 /* rngs.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2D91E48797500650E93 /* rngs.h */; };
D04BB33A1E48797500650E93 /* shader.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2DA1E48797500650E93 /* shader.c */; };
D04BB33B1E48797500650E93 /* shader.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2DB1E48797500650E93 /* shader.h */; };
D04BB33C1E48797500650E93 /* timing.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2DC1E48797500650E93 /* timing.c */; };
D04BB33D1E48797500650E93 /* timing.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2DD1E48797500650E93 /* timing.h */; };
D04BB33E1E48797500650E93 /* platform_log.c in Sources */ = {isa = PBXBuildFile; fileRef = D04BB2E01E48797500650E93 /* platform_log.c */; };
D04BB33F1E48797500650E93 /* platform_log.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2E11E48797500650E93 /* platform_log.h */; };
D04BB3401E48797500650E93 /* platform_macros.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2E21E48797500650E93 /* platform_macros.h */; };
D04BB3511E48797500650E93 /* platform_gl.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB2FE1E48797500650E93 /* platform_gl.h */; };
D04BB3521E48797500650E93 /* fast_arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3001E48797500650E93 /* fast_arrow@2x.png */; };
D04BB3531E48797500650E93 /* fast_arrow_shadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3011E48797500650E93 /* fast_arrow_shadow@2x.png */; };
D04BB3541E48797500650E93 /* fast_body@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3021E48797500650E93 /* fast_body@2x.png */; };
D04BB3551E48797500650E93 /* fast_spiral@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3031E48797500650E93 /* fast_spiral@2x.png */; };
D04BB3561E48797500650E93 /* ic_bubble@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3041E48797500650E93 /* ic_bubble@2x.png */; };
D04BB3571E48797500650E93 /* ic_bubble_dot@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3051E48797500650E93 /* ic_bubble_dot@2x.png */; };
D04BB3581E48797500650E93 /* ic_cam@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3061E48797500650E93 /* ic_cam@2x.png */; };
D04BB3591E48797500650E93 /* ic_cam_lens@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3071E48797500650E93 /* ic_cam_lens@2x.png */; };
D04BB35A1E48797500650E93 /* ic_pencil@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3081E48797500650E93 /* ic_pencil@2x.png */; };
D04BB35B1E48797500650E93 /* ic_pin@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3091E48797500650E93 /* ic_pin@2x.png */; };
D04BB35C1E48797500650E93 /* ic_smile@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30A1E48797500650E93 /* ic_smile@2x.png */; };
D04BB35D1E48797500650E93 /* ic_smile_eye@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30B1E48797500650E93 /* ic_smile_eye@2x.png */; };
D04BB35E1E48797500650E93 /* ic_videocam@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30C1E48797500650E93 /* ic_videocam@2x.png */; };
D04BB35F1E48797500650E93 /* knot_down@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30D1E48797500650E93 /* knot_down@2x.png */; };
D04BB3601E48797500650E93 /* knot_up@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30E1E48797500650E93 /* knot_up@2x.png */; };
D04BB3611E48797500650E93 /* powerful_infinity@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB30F1E48797500650E93 /* powerful_infinity@2x.png */; };
D04BB3621E48797500650E93 /* powerful_infinity_white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3101E48797500650E93 /* powerful_infinity_white@2x.png */; };
D04BB3631E48797500650E93 /* powerful_mask@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3111E48797500650E93 /* powerful_mask@2x.png */; };
D04BB3641E48797500650E93 /* powerful_star@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3121E48797500650E93 /* powerful_star@2x.png */; };
D04BB3651E48797500650E93 /* private_door@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3131E48797500650E93 /* private_door@2x.png */; };
D04BB3661E48797500650E93 /* private_screw@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3141E48797500650E93 /* private_screw@2x.png */; };
D04BB3671E48797500650E93 /* start_arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3151E48797500650E93 /* start_arrow@2x.png */; };
D04BB3681E48797500650E93 /* start_arrow_ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3161E48797500650E93 /* start_arrow_ipad.png */; };
D04BB3691E48797500650E93 /* start_arrow_ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3171E48797500650E93 /* start_arrow_ipad@2x.png */; };
D04BB36A1E48797500650E93 /* telegram_plane@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3181E48797500650E93 /* telegram_plane@2x.png */; };
D04BB36B1E48797500650E93 /* telegram_sphere@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D04BB3191E48797500650E93 /* telegram_sphere@2x.png */; };
D04BB36E1E48797500650E93 /* RMGeometry.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB31C1E48797500650E93 /* RMGeometry.h */; };
D04BB36F1E48797500650E93 /* RMGeometry.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB31D1E48797500650E93 /* RMGeometry.m */; };
D04BB3721E48797500650E93 /* RMIntroPageView.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB3201E48797500650E93 /* RMIntroPageView.h */; };
D04BB3731E48797500650E93 /* RMIntroPageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB3211E48797500650E93 /* RMIntroPageView.m */; };
D04BB3741E48797500650E93 /* RMIntroViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB3221E48797500650E93 /* RMIntroViewController.h */; };
D04BB3751E48797500650E93 /* RMIntroViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB3231E48797500650E93 /* RMIntroViewController.m */; };
D04BB3761E48797500650E93 /* RMLoginViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB3241E48797500650E93 /* RMLoginViewController.h */; };
D04BB3771E48797500650E93 /* RMLoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB3251E48797500650E93 /* RMLoginViewController.m */; };
D04BB3781E48797500650E93 /* RMRootViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB3261E48797500650E93 /* RMRootViewController.h */; };
D04BB3791E48797500650E93 /* RMRootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB3271E48797500650E93 /* RMRootViewController.m */; };
D04BB37A1E48797500650E93 /* texture_helper.h in Headers */ = {isa = PBXBuildFile; fileRef = D04BB3281E48797500650E93 /* texture_helper.h */; };
D04BB37B1E48797500650E93 /* texture_helper.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BB3291E48797500650E93 /* texture_helper.m */; };
D050F2131E48B61500988324 /* PhoneInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2121E48B61500988324 /* PhoneInputNode.swift */; };
D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */; };
D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */; };
D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; };
D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; };
D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; };
@ -170,6 +248,8 @@
D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; };
D0BC38631E3F9EFA0044D6FE /* EditableTokenListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BC38621E3F9EFA0044D6FE /* EditableTokenListNode.swift */; };
D0BC386A1E3FB94D0044D6FE /* CreateGroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BC38691E3FB94D0044D6FE /* CreateGroupController.swift */; };
D0BC387F1E40F1CF0044D6FE /* ContactSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BC387E1E40F1CF0044D6FE /* ContactSelectionController.swift */; };
D0BC38811E40F1D80044D6FE /* ContactSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BC38801E40F1D80044D6FE /* ContactSelectionControllerNode.swift */; };
D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */; };
D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */; };
D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */; };
@ -209,6 +289,8 @@
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */; };
D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */; };
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */; };
D0DA44541E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */; };
D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */; };
D0DC35441DE32230000195EB /* ChatInterfaceStateContextQueries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC35431DE32230000195EB /* ChatInterfaceStateContextQueries.swift */; };
D0DC35461DE35805000195EB /* MentionChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC35451DE35805000195EB /* MentionChatInputPanelItem.swift */; };
D0DC354A1DE366CD000195EB /* CommandChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC35491DE366CD000195EB /* CommandChatInputContextPanelNode.swift */; };
@ -419,6 +501,7 @@
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsAndSounds.swift; sourceTree = "<group>"; };
D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppNotificationSettings.swift; sourceTree = "<group>"; };
D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettings.swift; sourceTree = "<group>"; };
D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = "<group>"; };
D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingButton.swift; sourceTree = "<group>"; };
D0215D371E040F53001A0B1E /* InstantPageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNode.swift; sourceTree = "<group>"; };
D0215D391E041003001A0B1E /* InstantPageLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageLayout.swift; sourceTree = "<group>"; };
@ -468,7 +551,84 @@
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; };
D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = "<group>"; };
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = "<group>"; };
D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickerGridItem.swift; sourceTree = "<group>"; };
D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputRecentStickerPacksItem.swift; sourceTree = "<group>"; };
D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListRecentPeersListItem.swift; sourceTree = "<group>"; };
D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceController.swift; sourceTree = "<group>"; };
D04B66B71DD672D00049C3D2 /* GeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = "<group>"; };
D04BB2B21E44E56200650E93 /* AuthorizationSequenceSplashController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSplashController.swift; sourceTree = "<group>"; };
D04BB2B41E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePhoneEntryController.swift; sourceTree = "<group>"; };
D04BB2B81E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePhoneEntryControllerNode.swift; sourceTree = "<group>"; };
D04BB2BA1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSplashControllerNode.swift; sourceTree = "<group>"; };
D04BB2BD1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCodeEntryController.swift; sourceTree = "<group>"; };
D04BB2BF1E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCodeEntryControllerNode.swift; sourceTree = "<group>"; };
D04BB2C21E45020A00650E93 /* AuthorizationSequencePasswordEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePasswordEntryController.swift; sourceTree = "<group>"; };
D04BB2C41E45022C00650E93 /* AuthorizationSequencePasswordEntryControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePasswordEntryControllerNode.swift; sourceTree = "<group>"; };
D04BB2CA1E48797500650E93 /* linmath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = linmath.h; sourceTree = "<group>"; };
D04BB2CC1E48797500650E93 /* animations.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = animations.c; sourceTree = "<group>"; };
D04BB2CD1E48797500650E93 /* animations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = animations.h; sourceTree = "<group>"; };
D04BB2CE1E48797500650E93 /* buffer.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = buffer.c; sourceTree = "<group>"; };
D04BB2CF1E48797500650E93 /* buffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = buffer.h; sourceTree = "<group>"; };
D04BB2D01E48797500650E93 /* config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = config.h; sourceTree = "<group>"; };
D04BB2D11E48797500650E93 /* macros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = macros.h; sourceTree = "<group>"; };
D04BB2D21E48797500650E93 /* math_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = math_helper.h; sourceTree = "<group>"; };
D04BB2D31E48797500650E93 /* matrix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = matrix.h; sourceTree = "<group>"; };
D04BB2D41E48797500650E93 /* objects.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = objects.c; sourceTree = "<group>"; };
D04BB2D51E48797500650E93 /* objects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = objects.h; sourceTree = "<group>"; };
D04BB2D61E48797500650E93 /* program.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = program.c; sourceTree = "<group>"; };
D04BB2D71E48797500650E93 /* program.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = program.h; sourceTree = "<group>"; };
D04BB2D81E48797500650E93 /* rngs.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = rngs.c; sourceTree = "<group>"; };
D04BB2D91E48797500650E93 /* rngs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rngs.h; sourceTree = "<group>"; };
D04BB2DA1E48797500650E93 /* shader.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = shader.c; sourceTree = "<group>"; };
D04BB2DB1E48797500650E93 /* shader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = shader.h; sourceTree = "<group>"; };
D04BB2DC1E48797500650E93 /* timing.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = timing.c; sourceTree = "<group>"; };
D04BB2DD1E48797500650E93 /* timing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = timing.h; sourceTree = "<group>"; };
D04BB2E01E48797500650E93 /* platform_log.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = platform_log.c; sourceTree = "<group>"; };
D04BB2E11E48797500650E93 /* platform_log.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platform_log.h; sourceTree = "<group>"; };
D04BB2E21E48797500650E93 /* platform_macros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platform_macros.h; sourceTree = "<group>"; };
D04BB2FE1E48797500650E93 /* platform_gl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = platform_gl.h; sourceTree = "<group>"; };
D04BB3001E48797500650E93 /* fast_arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_arrow@2x.png"; sourceTree = "<group>"; };
D04BB3011E48797500650E93 /* fast_arrow_shadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_arrow_shadow@2x.png"; sourceTree = "<group>"; };
D04BB3021E48797500650E93 /* fast_body@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_body@2x.png"; sourceTree = "<group>"; };
D04BB3031E48797500650E93 /* fast_spiral@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_spiral@2x.png"; sourceTree = "<group>"; };
D04BB3041E48797500650E93 /* ic_bubble@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_bubble@2x.png"; sourceTree = "<group>"; };
D04BB3051E48797500650E93 /* ic_bubble_dot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_bubble_dot@2x.png"; sourceTree = "<group>"; };
D04BB3061E48797500650E93 /* ic_cam@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_cam@2x.png"; sourceTree = "<group>"; };
D04BB3071E48797500650E93 /* ic_cam_lens@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_cam_lens@2x.png"; sourceTree = "<group>"; };
D04BB3081E48797500650E93 /* ic_pencil@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_pencil@2x.png"; sourceTree = "<group>"; };
D04BB3091E48797500650E93 /* ic_pin@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_pin@2x.png"; sourceTree = "<group>"; };
D04BB30A1E48797500650E93 /* ic_smile@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_smile@2x.png"; sourceTree = "<group>"; };
D04BB30B1E48797500650E93 /* ic_smile_eye@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_smile_eye@2x.png"; sourceTree = "<group>"; };
D04BB30C1E48797500650E93 /* ic_videocam@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_videocam@2x.png"; sourceTree = "<group>"; };
D04BB30D1E48797500650E93 /* knot_down@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "knot_down@2x.png"; sourceTree = "<group>"; };
D04BB30E1E48797500650E93 /* knot_up@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "knot_up@2x.png"; sourceTree = "<group>"; };
D04BB30F1E48797500650E93 /* powerful_infinity@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity@2x.png"; sourceTree = "<group>"; };
D04BB3101E48797500650E93 /* powerful_infinity_white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity_white@2x.png"; sourceTree = "<group>"; };
D04BB3111E48797500650E93 /* powerful_mask@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_mask@2x.png"; sourceTree = "<group>"; };
D04BB3121E48797500650E93 /* powerful_star@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_star@2x.png"; sourceTree = "<group>"; };
D04BB3131E48797500650E93 /* private_door@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "private_door@2x.png"; sourceTree = "<group>"; };
D04BB3141E48797500650E93 /* private_screw@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "private_screw@2x.png"; sourceTree = "<group>"; };
D04BB3151E48797500650E93 /* start_arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow@2x.png"; sourceTree = "<group>"; };
D04BB3161E48797500650E93 /* start_arrow_ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = start_arrow_ipad.png; sourceTree = "<group>"; };
D04BB3171E48797500650E93 /* start_arrow_ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow_ipad@2x.png"; sourceTree = "<group>"; };
D04BB3181E48797500650E93 /* telegram_plane@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane@2x.png"; sourceTree = "<group>"; };
D04BB3191E48797500650E93 /* telegram_sphere@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_sphere@2x.png"; sourceTree = "<group>"; };
D04BB31C1E48797500650E93 /* RMGeometry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMGeometry.h; sourceTree = "<group>"; };
D04BB31D1E48797500650E93 /* RMGeometry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMGeometry.m; sourceTree = "<group>"; };
D04BB3201E48797500650E93 /* RMIntroPageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMIntroPageView.h; sourceTree = "<group>"; };
D04BB3211E48797500650E93 /* RMIntroPageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMIntroPageView.m; sourceTree = "<group>"; };
D04BB3221E48797500650E93 /* RMIntroViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMIntroViewController.h; sourceTree = "<group>"; };
D04BB3231E48797500650E93 /* RMIntroViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMIntroViewController.m; sourceTree = "<group>"; };
D04BB3241E48797500650E93 /* RMLoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMLoginViewController.h; sourceTree = "<group>"; };
D04BB3251E48797500650E93 /* RMLoginViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMLoginViewController.m; sourceTree = "<group>"; };
D04BB3261E48797500650E93 /* RMRootViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMRootViewController.h; sourceTree = "<group>"; };
D04BB3271E48797500650E93 /* RMRootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMRootViewController.m; sourceTree = "<group>"; };
D04BB3281E48797500650E93 /* texture_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = texture_helper.h; sourceTree = "<group>"; };
D04BB3291E48797500650E93 /* texture_helper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = texture_helper.m; sourceTree = "<group>"; };
D050F2121E48B61500988324 /* PhoneInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneInputNode.swift; sourceTree = "<group>"; };
D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionController.swift; sourceTree = "<group>"; };
D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionControllerNode.swift; sourceTree = "<group>"; };
D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = "<group>"; };
D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = "<group>"; };
D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = "<group>"; };
@ -550,6 +710,8 @@
D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = "<group>"; };
D0BC38621E3F9EFA0044D6FE /* EditableTokenListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableTokenListNode.swift; sourceTree = "<group>"; };
D0BC38691E3FB94D0044D6FE /* CreateGroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupController.swift; sourceTree = "<group>"; };
D0BC387E1E40F1CF0044D6FE /* ContactSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionController.swift; sourceTree = "<group>"; };
D0BC38801E40F1D80044D6FE /* ContactSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionControllerNode.swift; sourceTree = "<group>"; };
D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatButtonKeyboardInputNode.swift; sourceTree = "<group>"; };
D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBotInfoItem.swift; sourceTree = "<group>"; };
D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = "<group>"; };
@ -589,6 +751,8 @@
D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = "<group>"; };
D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = "<group>"; };
D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareRecipientsActionSheetController.swift; sourceTree = "<group>"; };
D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressNavigationButtonNode.swift; sourceTree = "<group>"; };
D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShakeAnimation.swift; sourceTree = "<group>"; };
D0DC35431DE32230000195EB /* ChatInterfaceStateContextQueries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextQueries.swift; sourceTree = "<group>"; };
D0DC35451DE35805000195EB /* MentionChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputPanelItem.swift; sourceTree = "<group>"; };
D0DC35491DE366CD000195EB /* CommandChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandChatInputContextPanelNode.swift; sourceTree = "<group>"; };
@ -843,6 +1007,7 @@
D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */,
D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */,
D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */,
D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */,
D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */,
D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */,
);
@ -889,6 +1054,185 @@
name = "Accessory Panels";
sourceTree = "<group>";
};
D049EAE01E447AB700A2CD3A /* Stickers */ = {
isa = PBXGroup;
children = (
D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */,
D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */,
);
name = Stickers;
sourceTree = "<group>";
};
D04BB2B61E44E5BB00650E93 /* Splash */ = {
isa = PBXGroup;
children = (
D04BB2B21E44E56200650E93 /* AuthorizationSequenceSplashController.swift */,
D04BB2BA1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift */,
);
name = Splash;
sourceTree = "<group>";
};
D04BB2B71E44E5CB00650E93 /* Phone Entry */ = {
isa = PBXGroup;
children = (
D04BB2B41E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift */,
D04BB2B81E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift */,
);
name = "Phone Entry";
sourceTree = "<group>";
};
D04BB2BC1E44FD1300650E93 /* Code Entry */ = {
isa = PBXGroup;
children = (
D04BB2BD1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift */,
D04BB2BF1E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift */,
);
name = "Code Entry";
sourceTree = "<group>";
};
D04BB2C11E45016800650E93 /* Password Entry */ = {
isa = PBXGroup;
children = (
D04BB2C21E45020A00650E93 /* AuthorizationSequencePasswordEntryController.swift */,
D04BB2C41E45022C00650E93 /* AuthorizationSequencePasswordEntryControllerNode.swift */,
);
name = "Password Entry";
sourceTree = "<group>";
};
D04BB2C61E48797500650E93 /* RMIntro */ = {
isa = PBXGroup;
children = (
D04BB2C71E48797500650E93 /* 3rdparty */,
D04BB2CB1E48797500650E93 /* core */,
D04BB2DE1E48797500650E93 /* platform */,
);
name = RMIntro;
path = "third-party/RMIntro";
sourceTree = SOURCE_ROOT;
};
D04BB2C71E48797500650E93 /* 3rdparty */ = {
isa = PBXGroup;
children = (
D04BB2C91E48797500650E93 /* linmath */,
);
path = 3rdparty;
sourceTree = "<group>";
};
D04BB2C91E48797500650E93 /* linmath */ = {
isa = PBXGroup;
children = (
D04BB2CA1E48797500650E93 /* linmath.h */,
);
path = linmath;
sourceTree = "<group>";
};
D04BB2CB1E48797500650E93 /* core */ = {
isa = PBXGroup;
children = (
D04BB2CC1E48797500650E93 /* animations.c */,
D04BB2CD1E48797500650E93 /* animations.h */,
D04BB2CE1E48797500650E93 /* buffer.c */,
D04BB2CF1E48797500650E93 /* buffer.h */,
D04BB2D01E48797500650E93 /* config.h */,
D04BB2D11E48797500650E93 /* macros.h */,
D04BB2D21E48797500650E93 /* math_helper.h */,
D04BB2D31E48797500650E93 /* matrix.h */,
D04BB2D41E48797500650E93 /* objects.c */,
D04BB2D51E48797500650E93 /* objects.h */,
D04BB2D61E48797500650E93 /* program.c */,
D04BB2D71E48797500650E93 /* program.h */,
D04BB2D81E48797500650E93 /* rngs.c */,
D04BB2D91E48797500650E93 /* rngs.h */,
D04BB2DA1E48797500650E93 /* shader.c */,
D04BB2DB1E48797500650E93 /* shader.h */,
D04BB2DC1E48797500650E93 /* timing.c */,
D04BB2DD1E48797500650E93 /* timing.h */,
);
path = core;
sourceTree = "<group>";
};
D04BB2DE1E48797500650E93 /* platform */ = {
isa = PBXGroup;
children = (
D04BB2DF1E48797500650E93 /* common */,
D04BB2E31E48797500650E93 /* ios */,
);
path = platform;
sourceTree = "<group>";
};
D04BB2DF1E48797500650E93 /* common */ = {
isa = PBXGroup;
children = (
D04BB2E01E48797500650E93 /* platform_log.c */,
D04BB2E11E48797500650E93 /* platform_log.h */,
D04BB2E21E48797500650E93 /* platform_macros.h */,
);
path = common;
sourceTree = "<group>";
};
D04BB2E31E48797500650E93 /* ios */ = {
isa = PBXGroup;
children = (
D04BB2FE1E48797500650E93 /* platform_gl.h */,
D04BB2FF1E48797500650E93 /* Resources */,
D04BB31C1E48797500650E93 /* RMGeometry.h */,
D04BB31D1E48797500650E93 /* RMGeometry.m */,
D04BB3201E48797500650E93 /* RMIntroPageView.h */,
D04BB3211E48797500650E93 /* RMIntroPageView.m */,
D04BB3221E48797500650E93 /* RMIntroViewController.h */,
D04BB3231E48797500650E93 /* RMIntroViewController.m */,
D04BB3241E48797500650E93 /* RMLoginViewController.h */,
D04BB3251E48797500650E93 /* RMLoginViewController.m */,
D04BB3261E48797500650E93 /* RMRootViewController.h */,
D04BB3271E48797500650E93 /* RMRootViewController.m */,
D04BB3281E48797500650E93 /* texture_helper.h */,
D04BB3291E48797500650E93 /* texture_helper.m */,
);
path = ios;
sourceTree = "<group>";
};
D04BB2FF1E48797500650E93 /* Resources */ = {
isa = PBXGroup;
children = (
D04BB3001E48797500650E93 /* fast_arrow@2x.png */,
D04BB3011E48797500650E93 /* fast_arrow_shadow@2x.png */,
D04BB3021E48797500650E93 /* fast_body@2x.png */,
D04BB3031E48797500650E93 /* fast_spiral@2x.png */,
D04BB3041E48797500650E93 /* ic_bubble@2x.png */,
D04BB3051E48797500650E93 /* ic_bubble_dot@2x.png */,
D04BB3061E48797500650E93 /* ic_cam@2x.png */,
D04BB3071E48797500650E93 /* ic_cam_lens@2x.png */,
D04BB3081E48797500650E93 /* ic_pencil@2x.png */,
D04BB3091E48797500650E93 /* ic_pin@2x.png */,
D04BB30A1E48797500650E93 /* ic_smile@2x.png */,
D04BB30B1E48797500650E93 /* ic_smile_eye@2x.png */,
D04BB30C1E48797500650E93 /* ic_videocam@2x.png */,
D04BB30D1E48797500650E93 /* knot_down@2x.png */,
D04BB30E1E48797500650E93 /* knot_up@2x.png */,
D04BB30F1E48797500650E93 /* powerful_infinity@2x.png */,
D04BB3101E48797500650E93 /* powerful_infinity_white@2x.png */,
D04BB3111E48797500650E93 /* powerful_mask@2x.png */,
D04BB3121E48797500650E93 /* powerful_star@2x.png */,
D04BB3131E48797500650E93 /* private_door@2x.png */,
D04BB3141E48797500650E93 /* private_screw@2x.png */,
D04BB3151E48797500650E93 /* start_arrow@2x.png */,
D04BB3161E48797500650E93 /* start_arrow_ipad.png */,
D04BB3171E48797500650E93 /* start_arrow_ipad@2x.png */,
D04BB3181E48797500650E93 /* telegram_plane@2x.png */,
D04BB3191E48797500650E93 /* telegram_sphere@2x.png */,
);
path = Resources;
sourceTree = "<group>";
};
D050F2141E48D9C200988324 /* Country Selection */ = {
isa = PBXGroup;
children = (
D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */,
D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */,
);
name = "Country Selection";
sourceTree = "<group>";
};
D0736F261DF4D2F300F2C02A /* Telegram Controller */ = {
isa = PBXGroup;
children = (
@ -912,6 +1256,7 @@
D07551891DDA4C7C0073E051 /* Legacy Components */ = {
isa = PBXGroup;
children = (
D04BB2C61E48797500650E93 /* RMIntro */,
D075518A1DDA4D7D0073E051 /* LegacyController.swift */,
D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */,
D07551921DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift */,
@ -1108,11 +1453,21 @@
children = (
D087750E1E3F469700A97350 /* Compose */,
D087751A1E3F540900A97350 /* Contact Multiselection */,
D0BC387D1E40F1B90044D6FE /* Contact Selection */,
D0BC38671E3FB9190044D6FE /* Create Group */,
);
name = Compose;
sourceTree = "<group>";
};
D0BC387D1E40F1B90044D6FE /* Contact Selection */ = {
isa = PBXGroup;
children = (
D0BC387E1E40F1CF0044D6FE /* ContactSelectionController.swift */,
D0BC38801E40F1D80044D6FE /* ContactSelectionControllerNode.swift */,
);
name = "Contact Selection";
sourceTree = "<group>";
};
D0C932341E0988AD0074F044 /* Button Keyboard */ = {
isa = PBXGroup;
children = (
@ -1270,6 +1625,7 @@
isa = PBXGroup;
children = (
D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */,
D049EAE01E447AB700A2CD3A /* Stickers */,
D0DF0C9F1D8219C7008AEB01 /* Hashtags */,
D0DF0CA21D82BCBC008AEB01 /* Mentions */,
D0DC35481DE366B4000195EB /* Commands */,
@ -1452,6 +1808,8 @@
D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */,
D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */,
D0BC38621E3F9EFA0044D6FE /* EditableTokenListNode.swift */,
D050F2121E48B61500988324 /* PhoneInputNode.swift */,
D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */,
);
name = Nodes;
sourceTree = "<group>";
@ -1520,6 +1878,12 @@
D0F69DE71D6B8A590046BCD6 /* Authorization */ = {
isa = PBXGroup;
children = (
D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */,
D04BB2B61E44E5BB00650E93 /* Splash */,
D050F2141E48D9C200988324 /* Country Selection */,
D04BB2B71E44E5CB00650E93 /* Phone Entry */,
D04BB2BC1E44FD1300650E93 /* Code Entry */,
D04BB2C11E45016800650E93 /* Password Entry */,
D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */,
D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */,
D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */,
@ -1555,6 +1919,7 @@
D0F69E061D6B8A930046BCD6 /* Recent Peers */ = {
isa = PBXGroup;
children = (
D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */,
D0F69E0B1D6B8AB10046BCD6 /* HorizontalPeerItem.swift */,
D0F69E091D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift */,
);
@ -1756,6 +2121,8 @@
D017494D1E1059570057C89A /* StringWithAppliedEntities.swift */,
D01749541E1082770057C89A /* StoredMessageFromSearchPeer.swift */,
D087750B1E3E7B7600A97350 /* PreferencesKeys.swift */,
D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */,
D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */,
);
name = Utils;
sourceTree = "<group>";
@ -1827,25 +2194,46 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
D04BB3401E48797500650E93 /* platform_macros.h in Headers */,
D04BB3391E48797500650E93 /* rngs.h in Headers */,
D04BB37A1E48797500650E93 /* texture_helper.h in Headers */,
D0D03B161DECB0FE00220C46 /* opus.h in Headers */,
D04BB36E1E48797500650E93 /* RMGeometry.h in Headers */,
D04BB3311E48797500650E93 /* macros.h in Headers */,
D0D03B141DECB0FE00220C46 /* ogg.h in Headers */,
D04BB3331E48797500650E93 /* matrix.h in Headers */,
D04BB3511E48797500650E93 /* platform_gl.h in Headers */,
D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */,
D04BB33D1E48797500650E93 /* timing.h in Headers */,
D04BB3351E48797500650E93 /* objects.h in Headers */,
D0D03B231DECB1AD00220C46 /* TGDataItem.h in Headers */,
D0D03B1F1DECB0FE00220C46 /* opusfile.h in Headers */,
D04BB32D1E48797500650E93 /* animations.h in Headers */,
D04BB3781E48797500650E93 /* RMRootViewController.h in Headers */,
D04BB32F1E48797500650E93 /* buffer.h in Headers */,
D04BB3371E48797500650E93 /* program.h in Headers */,
D0D03B1D1DECB0FE00220C46 /* internal.h in Headers */,
D0FC40901D5B8E7500261D9D /* TelegramUI.h in Headers */,
D04BB3741E48797500650E93 /* RMIntroViewController.h in Headers */,
D0D03B0B1DECB0FE00220C46 /* opus_header.h in Headers */,
D0D03B181DECB0FE00220C46 /* opus_multistream.h in Headers */,
D04BB33F1E48797500650E93 /* platform_log.h in Headers */,
D0D03B111DECB0FE00220C46 /* wav_io.h in Headers */,
D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */,
D04BB32B1E48797500650E93 /* linmath.h in Headers */,
D04BB33B1E48797500650E93 /* shader.h in Headers */,
D0D03B0C1DECB0FE00220C46 /* opusenc.h in Headers */,
D0D03B0F1DECB0FE00220C46 /* picture.h in Headers */,
D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */,
D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */,
D0D03B151DECB0FE00220C46 /* os_types.h in Headers */,
D0D03B191DECB0FE00220C46 /* opus_types.h in Headers */,
D04BB3301E48797500650E93 /* config.h in Headers */,
D04BB3721E48797500650E93 /* RMIntroPageView.h in Headers */,
D0D03B171DECB0FE00220C46 /* opus_defines.h in Headers */,
D0D03B091DECB0FE00220C46 /* diag_range.h in Headers */,
D04BB3321E48797500650E93 /* math_helper.h in Headers */,
D04BB3761E48797500650E93 /* RMLoginViewController.h in Headers */,
D00C7CF71E37BF680080C3D5 /* SecretChatKeyVisualization.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1918,6 +2306,7 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = D0FC40751D5B8E7400261D9D;
productRefGroup = D0FC40801D5B8E7400261D9D /* Products */;
@ -1935,9 +2324,35 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D04BB3541E48797500650E93 /* fast_body@2x.png in Resources */,
D04BB35A1E48797500650E93 /* ic_pencil@2x.png in Resources */,
D04BB3611E48797500650E93 /* powerful_infinity@2x.png in Resources */,
D04BB3681E48797500650E93 /* start_arrow_ipad.png in Resources */,
D04BB3521E48797500650E93 /* fast_arrow@2x.png in Resources */,
D04BB3561E48797500650E93 /* ic_bubble@2x.png in Resources */,
D04BB35C1E48797500650E93 /* ic_smile@2x.png in Resources */,
D04BB3661E48797500650E93 /* private_screw@2x.png in Resources */,
D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */,
D04BB3601E48797500650E93 /* knot_up@2x.png in Resources */,
D04BB3651E48797500650E93 /* private_door@2x.png in Resources */,
D04BB3671E48797500650E93 /* start_arrow@2x.png in Resources */,
D04BB36A1E48797500650E93 /* telegram_plane@2x.png in Resources */,
D04BB3641E48797500650E93 /* powerful_star@2x.png in Resources */,
D04BB35D1E48797500650E93 /* ic_smile_eye@2x.png in Resources */,
D04BB35F1E48797500650E93 /* knot_down@2x.png in Resources */,
D04BB36B1E48797500650E93 /* telegram_sphere@2x.png in Resources */,
D04BB3551E48797500650E93 /* fast_spiral@2x.png in Resources */,
D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */,
D04BB35E1E48797500650E93 /* ic_videocam@2x.png in Resources */,
D04BB35B1E48797500650E93 /* ic_pin@2x.png in Resources */,
D04BB3571E48797500650E93 /* ic_bubble_dot@2x.png in Resources */,
D04BB3581E48797500650E93 /* ic_cam@2x.png in Resources */,
D04BB3621E48797500650E93 /* powerful_infinity_white@2x.png in Resources */,
D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */,
D04BB3591E48797500650E93 /* ic_cam_lens@2x.png in Resources */,
D04BB3631E48797500650E93 /* powerful_mask@2x.png in Resources */,
D04BB3531E48797500650E93 /* fast_arrow_shadow@2x.png in Resources */,
D04BB3691E48797500650E93 /* start_arrow_ipad@2x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1966,10 +2381,12 @@
D0D03B1B1DECB0FE00220C46 /* info.c in Sources */,
D0215D4A1E041CAF001A0B1E /* InstantPageMediaItem.swift in Sources */,
D087751E1E3F579300A97350 /* CounterContollerTitleView.swift in Sources */,
D050F2131E48B61500988324 /* PhoneInputNode.swift in Sources */,
D08775141E3F4A7700A97350 /* ContactListNameIndexHeader.swift in Sources */,
D0215D461E041851001A0B1E /* InstantPageTextItem.swift in Sources */,
D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */,
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */,
D0BC38811E40F1D80044D6FE /* ContactSelectionControllerNode.swift in Sources */,
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
D07827C71E01CD5900071108 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */,
@ -1979,6 +2396,7 @@
D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */,
D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */,
D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */,
D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */,
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */,
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */,
D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */,
@ -1986,8 +2404,10 @@
D0D2686C1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift in Sources */,
D0215D541E043018001A0B1E /* InstantPageController.swift in Sources */,
D00C7CE91E379B820080C3D5 /* ChatSecretAutoremoveTimerActionSheet.swift in Sources */,
D04BB3361E48797500650E93 /* program.c in Sources */,
D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */,
D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */,
D04BB2C51E45022C00650E93 /* AuthorizationSequencePasswordEntryControllerNode.swift in Sources */,
D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */,
D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */,
D00C7CF81E37BF680080C3D5 /* SecretChatKeyVisualization.m in Sources */,
@ -2025,11 +2445,13 @@
D0F69DE01D6B8A420046BCD6 /* ListControllerButtonItem.swift in Sources */,
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */,
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */,
D04BB2B91E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */,
D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */,
D003702E1DA43052004308D3 /* ItemListAvatarAndNameItem.swift in Sources */,
D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */,
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */,
D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */,
D049EAE41E44949F00A2CD3A /* HorizontalStickerGridItem.swift in Sources */,
D0736F231DF496D000F2C02A /* PeerMediaAudioPlaylist.swift in Sources */,
D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */,
D0D03B131DECB0FE00220C46 /* framing.c in Sources */,
@ -2047,12 +2469,15 @@
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
D01B279D1E394A500022A4C0 /* NotificationsAndSounds.swift in Sources */,
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
D04BB3731E48797500650E93 /* RMIntroPageView.m in Sources */,
D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */,
D099EA1F1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */,
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */,
D04BB3341E48797500650E93 /* objects.c in Sources */,
D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */,
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
D0EFD8961DDE8249009E508A /* LegacyLocationPicker.swift in Sources */,
D04BB3771E48797500650E93 /* RMLoginViewController.m in Sources */,
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */,
D0B843921DA7F13E005F29E1 /* ItemListDisclosureItem.swift in Sources */,
D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */,
@ -2074,16 +2499,19 @@
D0215D3E1E041048001A0B1E /* InstantPageMedia.swift in Sources */,
D00C7CD91E36B2DB0080C3D5 /* ContactListNode.swift in Sources */,
D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */,
D04BB32E1E48797500650E93 /* buffer.c in Sources */,
D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */,
D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */,
D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */,
D0736F2A1DF4D5FF00F2C02A /* MediaNavigationAccessoryPanel.swift in Sources */,
D087751C1E3F542500A97350 /* ContactMultiselectionControllerNode.swift in Sources */,
D0E7A1BD1D8C246D00C37A6F /* ChatHistoryListNode.swift in Sources */,
D049EAE21E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift in Sources */,
D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */,
D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */,
D0215D501E0422C7001A0B1E /* InstantPageWebEmbedItem.swift in Sources */,
D0BC38631E3F9EFA0044D6FE /* EditableTokenListNode.swift in Sources */,
D049EAE61E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift in Sources */,
D023ED2E1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift in Sources */,
D02383771DDF16B2004018B6 /* ChatControllerTitlePanelNodeContainer.swift in Sources */,
D00370321DA46C06004308D3 /* ItemListTextWithLabelItem.swift in Sources */,
@ -2106,6 +2534,7 @@
D02383731DDF0D8A004018B6 /* ChatInfoTitlePanelNode.swift in Sources */,
D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */,
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */,
D04BB37B1E48797500650E93 /* texture_helper.m in Sources */,
D0F69EA31D6B8E380046BCD6 /* StickerResources.swift in Sources */,
D0DE77321D940295002B8809 /* ListMessageFileItemNode.swift in Sources */,
D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */,
@ -2114,6 +2543,7 @@
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */,
D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */,
D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */,
D04BB33A1E48797500650E93 /* shader.c in Sources */,
D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */,
D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */,
D0F69D671D6B87D30046BCD6 /* FFMpegPacket.swift in Sources */,
@ -2139,6 +2569,7 @@
D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */,
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */,
D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */,
D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */,
D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */,
D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */,
@ -2149,6 +2580,7 @@
D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */,
D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */,
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */,
D01D6BFC1E42AB3C006151C6 /* EmojiUtils.swift in Sources */,
D02383841DDFA22C004018B6 /* ListMessageHoleItem.swift in Sources */,
D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */,
D0DE76F71D91BA3D002B8809 /* GridHoleItem.swift in Sources */,
@ -2158,6 +2590,7 @@
D0F69E021D6B8A880046BCD6 /* ChatListHoleItem.swift in Sources */,
D0F917B51E0DA396003687E6 /* GenerateTextEntities.swift in Sources */,
D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */,
D04BB33E1E48797500650E93 /* platform_log.c in Sources */,
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */,
D01B279B1E39386C0022A4C0 /* SettingsControllerEntries.swift in Sources */,
D08775201E3F595000A97350 /* ContactListActionItem.swift in Sources */,
@ -2177,9 +2610,11 @@
D0DE77271D932627002B8809 /* ChatHistoryNode.swift in Sources */,
D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */,
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */,
D04BB36F1E48797500650E93 /* RMGeometry.m in Sources */,
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */,
D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */,
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */,
D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */,
D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */,
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */,
D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */,
@ -2187,9 +2622,14 @@
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */,
D0E7A1C11D8C258D00C37A6F /* ChatHistoryEntriesForView.swift in Sources */,
D01749511E1067E40057C89A /* HashtagSearchController.swift in Sources */,
D04BB3381E48797500650E93 /* rngs.c in Sources */,
D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */,
D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */,
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */,
D04BB33C1E48797500650E93 /* timing.c in Sources */,
D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */,
D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */,
D049EAF31E44DE2500A2CD3A /* AuthorizationSequenceController.swift in Sources */,
D08774FA1E3E2A5600A97350 /* ItemListCheckboxItem.swift in Sources */,
D073CE711DCBF23F007511FD /* DeclareEncodables.swift in Sources */,
D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */,
@ -2202,7 +2642,11 @@
D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */,
D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */,
D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */,
D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */,
D01749551E1082770057C89A /* StoredMessageFromSearchPeer.swift in Sources */,
D04BB32C1E48797500650E93 /* animations.c in Sources */,
D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */,
D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */,
D0215D581E04302E001A0B1E /* InstantPageTileNode.swift in Sources */,
D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */,
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */,
@ -2211,7 +2655,10 @@
D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */,
D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */,
D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */,
D04BB3791E48797500650E93 /* RMRootViewController.m in Sources */,
D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */,
D04BB2B31E44E56200650E93 /* AuthorizationSequenceSplashController.swift in Sources */,
D04BB3751E48797500650E93 /* RMIntroViewController.m in Sources */,
D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */,
D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */,
D0177B821DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift in Sources */,
@ -2236,10 +2683,13 @@
D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */,
D0DC35441DE32230000195EB /* ChatInterfaceStateContextQueries.swift in Sources */,
D0215D4C1E041D5E001A0B1E /* InstantPageMediaNode.swift in Sources */,
D0DA44541E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift in Sources */,
D04BB2C31E45020A00650E93 /* AuthorizationSequencePasswordEntryController.swift in Sources */,
D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */,
D023ED321DDB60CF00BD496D /* LegacyNavigationController.swift in Sources */,
D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */,
D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */,
D0BC387F1E40F1CF0044D6FE /* ContactSelectionController.swift in Sources */,
D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */,
D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */,
D0215D3A1E041003001A0B1E /* InstantPageLayout.swift in Sources */,
@ -2360,6 +2810,10 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/RMIntro/platform/ios/HockeySDK-iOS/HockeySDK.embeddedframework",
);
HEADER_SEARCH_PATHS = "third-party/ogg";
INFOPLIST_FILE = TelegramUI/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -2509,6 +2963,10 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/RMIntro/platform/ios/HockeySDK-iOS/HockeySDK.embeddedframework",
);
HEADER_SEARCH_PATHS = "third-party/ogg";
INFOPLIST_FILE = TelegramUI/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -2545,6 +3003,10 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/third-party/RMIntro/platform/ios/HockeySDK-iOS/HockeySDK.embeddedframework",
);
HEADER_SEARCH_PATHS = "third-party/ogg";
INFOPLIST_FILE = TelegramUI/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";

View File

@ -7,7 +7,7 @@
<key>TelegramUI.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>19</integer>
<integer>12</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -6,7 +6,7 @@ import TelegramCore
enum AuthorizationCodeResult {
case Authorization(Api.auth.Authorization)
case Password
case Password(String)
}
class AuthorizationCodeController: ViewController {
@ -62,23 +62,5 @@ class AuthorizationCodeController: ViewController {
default:
break
}
if let phoneCodeHash = phoneCodeHash {
let signal = self.account.network.request(Api.functions.auth.signIn(phoneNumber: self.phone, phoneCodeHash: phoneCodeHash, phoneCode: node.codeNode.attributedText?.string ?? "")) |> map { authorization in
return AuthorizationCodeResult.Authorization(authorization)
} |> `catch` { error -> Signal<AuthorizationCodeResult, MTRpcError> in
switch (error.errorCode, error.errorDescription) {
case (401, "SESSION_PASSWORD_NEEDED"):
return .single(.Password)
case _:
return .fail(error)
}
}
self.signInDisposable.set(signal.start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.resultPipe.putNext(result)
}
}))
}
}
}

View File

@ -60,7 +60,7 @@ public class AuthorizationController: NavigationController {
modifier.setState(state)
return state
} |> map { state -> Account in
let account = Account(id: account.id, basePath: account.basePath, testingEnvironment: account.testingEnvironment, postbox: account.postbox, network: account.network, peerId: user.id)
let account = Account(id: account.id, basePath: account.basePath, logger: account.logger, testingEnvironment: account.testingEnvironment, postbox: account.postbox, network: account.network, peerId: user.id)
account.shouldBeServiceTaskMaster.set(.single(.always))
return account
}

View File

@ -48,7 +48,7 @@ class AuthorizationPhoneController: ViewController {
@objc func nextPressed() {
let phone = self.node.phoneNode.attributedText?.string ?? ""
let account = self.account
let sendCode = Api.functions.auth.sendCode(flags: 0, phoneNumber: phone, currentNumber: nil, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb", langCode: "en")
let sendCode = Api.functions.auth.sendCode(flags: 0, phoneNumber: phone, currentNumber: nil, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb")
let signal = account.network.request(sendCode)
|> map { result in

View File

@ -0,0 +1,91 @@
import Foundation
import Display
import AsyncDisplayKit
import TelegramCore
final class AuthorizationSequenceCodeEntryController: ViewController {
private var controllerNode: AuthorizationSequenceCodeEntryControllerNode {
return self.displayNode as! AuthorizationSequenceCodeEntryControllerNode
}
var loginWithCode: ((String) -> Void)?
var requestNextOption: (() -> Void)?
var data: (String, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?)?
private let hapticFeedback = HapticFeedback()
var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode())
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
self.controllerNode.inProgress = self.inProgress
}
}
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceCodeEntryControllerNode()
self.displayNodeDidLoad()
self.controllerNode.loginWithCode = { [weak self] code in
self?.loginWithCode?(code)
}
self.controllerNode.requestNextOption = { [weak self] in
self?.requestNextOption?()
}
if let (number, codeType, nextType, timeout) = self.data {
self.controllerNode.updateData(number: number, codeType: codeType, nextType: nextType, timeout: timeout)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.activateInput()
}
func updateData(number: String, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) {
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 {
self.controllerNode.updateData(number: number, codeType: codeType, nextType: nextType, timeout: timeout)
self.requestLayout(transition: .immediate)
}
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
}
@objc func nextPressed() {
if self.controllerNode.currentCode.isEmpty {
hapticFeedback.error()
self.controllerNode.animateError()
} else {
self.loginWithCode?(self.controllerNode.currentCode)
}
}
}

View File

@ -0,0 +1,278 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
private func currentOptionText(_ type: SentAuthorizationCodeType) -> NSAttributedString {
switch type {
case .sms:
return NSAttributedString(string: "We have sent you an SMS with a code to the number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
case .otherSession:
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: "We've sent the code to the ", font: Font.regular(16.0), textColor: UIColor.black))
string.append(NSAttributedString(string: "Telegram", font: Font.medium(16.0), textColor: UIColor.black))
string.append(NSAttributedString(string: " app on your other device.", font: Font.regular(16.0), textColor: UIColor.black))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
string.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSMakeRange(0, string.length))
return string
case .call, .flashCall:
return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
}
}
private func nextOptionText(_ type: AuthorizationCodeNextType?, timeout: Int32?) -> NSAttributedString {
if let type = type, let timeout = timeout {
let minutes = timeout / 60
let seconds = timeout % 60
switch type {
case .sms:
if timeout <= 0 {
return NSAttributedString(string: "Telegram sent you an SMS", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
} else {
return NSAttributedString(string: String(format: "Telegram will send you an SMS in %d:%.2d", minutes, seconds), font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
}
case .call, .flashCall:
if timeout <= 0 {
return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
} else {
return NSAttributedString(string: String(format: "Telegram will call you in %d:%.2d", minutes, seconds), font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
}
}
} else {
return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center)
}
}
final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let navigationBackgroundNode: ASDisplayNode
private let stripeNode: ASDisplayNode
private let titleNode: ASTextNode
private let currentOptionNode: ASTextNode
private let nextOptionNode: ASTextNode
private let codeField: TextFieldNode
private let codeSeparatorNode: ASDisplayNode
private var codeType: SentAuthorizationCodeType?
private let countdownDisposable = MetaDisposable()
private var currentTimeoutTime: Int32?
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var phoneNumber: String = "" {
didSet {
self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: UIColor.black)
}
}
var currentCode: String {
return self.codeField.textField.text ?? ""
}
var loginWithCode: ((String) -> Void)?
var requestNextOption: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.codeField.alpha = self.inProgress ? 0.6 : 1.0
}
}
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
self.titleNode.displaysAsynchronously = false
self.currentOptionNode = ASTextNode()
self.currentOptionNode.isLayerBacked = true
self.currentOptionNode.displaysAsynchronously = false
self.nextOptionNode = ASTextNode()
self.nextOptionNode.isLayerBacked = true
self.nextOptionNode.displaysAsynchronously = false
self.nextOptionNode.attributedText = nextOptionText(AuthorizationCodeNextType.call, timeout: 60)
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(24.0)
self.codeField.textField.textAlignment = .center
self.codeField.textField.keyboardType = .numberPad
self.codeField.textField.returnKeyType = .done
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
self.addSubnode(self.navigationBackgroundNode)
self.addSubnode(self.stripeNode)
self.addSubnode(self.codeSeparatorNode)
self.addSubnode(self.codeField)
self.addSubnode(self.titleNode)
self.addSubnode(self.currentOptionNode)
self.addSubnode(self.nextOptionNode)
self.codeField.textField.addTarget(self, action: #selector(self.codeFieldTextChanged(_:)), for: .editingChanged)
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(0xbcbcc3))
}
deinit {
self.countdownDisposable.dispose()
}
func updateData(number: String, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) {
self.codeType = codeType
self.phoneNumber = number
self.currentOptionNode.attributedText = currentOptionText(codeType)
if let timeout = timeout {
self.currentTimeoutTime = timeout
let disposable = ((Signal<Int, NoError>.single(1) |> delay(1.0, queue: Queue.mainQueue())) |> restart).start(next: { [weak self] _ in
if let strongSelf = self {
if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 {
strongSelf.currentTimeoutTime = currentTimeoutTime - 1
strongSelf.nextOptionNode.attributedText = nextOptionText(nextType, timeout:strongSelf.currentTimeoutTime)
if let layoutArguments = strongSelf.layoutArguments {
strongSelf.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate)
}
if currentTimeoutTime == 1 {
strongSelf.requestNextOption?()
}
}
}
})
self.countdownDisposable.set(disposable)
} else {
self.currentTimeoutTime = nil
self.countdownDisposable.set(nil)
}
self.nextOptionNode.attributedText = nextOptionText(nextType, timeout: self.currentTimeoutTime)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.layoutArguments = (layout, navigationBarHeight)
let insets = layout.insets(options: [.statusBar, .input])
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: self.phoneNumber, font: Font.light(30.0), textColor: UIColor.black)
} else {
self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.regular(20.0), textColor: UIColor.black)
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let additionalTitleSpacing: CGFloat
if titleSize.width > layout.size.width - 160.0 {
additionalTitleSpacing = 44.0
} else {
additionalTitleSpacing = 0.0
}
let minimalTitleSpacing: CGFloat = 10.0
let maxTitleSpacing: CGFloat = 22.0
let inputFieldsHeight: CGFloat = 60.0
let minimalNoticeSpacing: CGFloat = 11.0
let maxNoticeSpacing: CGFloat = 35.0
let noticeSize = self.currentOptionNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
let minimalTermsOfServiceSpacing: CGFloat = 6.0
let maxTermsOfServiceSpacing: CGFloat = 20.0
let termsOfServiceSize = self.nextOptionNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let minTrailingSpacing: CGFloat = 10.0
let inputHeight = inputFieldsHeight
let essentialHeight = additionalTitleSpacing + titleSize.height + minimalTitleSpacing + inputHeight + minimalNoticeSpacing + noticeSize.height
let additionalHeight = minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing
let navigationHeight: CGFloat
if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight {
navigationHeight = min(floor(availableHeight * 0.3), availableHeight - inputFieldsHeight)
} else {
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)
} else {
titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height)
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize))
let codeFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + 3.0), size: CGSize(width: layout.size.width, height: 60.0))
transition.updateFrame(node: self.codeField, frame: codeFieldFrame)
transition.updateFrame(node: self.codeSeparatorNode, frame: CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 60.0), size: CGSize(width: layout.size.width - 44.0, height: UIScreenPixel)))
let additionalAvailableHeight = max(1.0, availableHeight - codeFieldFrame.maxY)
let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height)
let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let noticeSpacing: CGFloat
let termsOfServiceSpacing: CGFloat
if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing {
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0)
} else {
noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing)
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
}
let currentOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: codeFieldFrame.maxY + noticeSpacing), size: noticeSize)
let nextOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: currentOptionFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize)
transition.updateFrame(node: self.currentOptionNode, frame: currentOptionFrame)
transition.updateFrame(node: self.nextOptionNode, frame: nextOptionFrame)
}
func activateInput() {
self.codeField.textField.becomeFirstResponder()
}
func animateError() {
self.codeField.layer.addShakeAnimation()
}
@objc func codeFieldTextChanged(_ textField: UITextField) {
if let codeType = self.codeType {
var codeLength: Int32?
switch codeType {
case let .call(length):
codeLength = length
case let .otherSession(length):
codeLength = length
case let .sms(length):
codeLength = length
default:
break
}
if let codeLength = codeLength, let text = textField.text, text.characters.count == Int(codeLength) {
self.loginWithCode?(text)
}
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return !self.inProgress
}
}

View File

@ -0,0 +1,326 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import MtProtoKitDynamic
public final class AuthorizationSequenceController: NavigationController {
private var account: UnauthorizedAccount
private var stateDisposable: Disposable?
private let actionDisposable = MetaDisposable()
let _authorizedAccount = Promise<Account>()
public var authorizedAccount: Signal<Account, NoError> {
return self._authorizedAccount.get()
}
public init(account: UnauthorizedAccount) {
self.account = account
super.init(nibName: nil, bundle: nil)
self.stateDisposable = (account.postbox.stateView() |> deliverOnMainQueue).start(next: { [weak self] view in
self?.updateState(state: view.state ?? UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty))
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stateDisposable?.dispose()
self.actionDisposable.dispose()
}
private func splashController() -> AuthorizationSequenceSplashController {
var currentController: AuthorizationSequenceSplashController?
for c in self.viewControllers {
if let c = c as? AuthorizationSequenceSplashController {
currentController = c
break
}
}
let controller: AuthorizationSequenceSplashController
if let currentController = currentController {
controller = currentController
} else {
controller = AuthorizationSequenceSplashController()
controller.nextPressed = { [weak self] in
if let strongSelf = self {
let masterDatacenterId = strongSelf.account.masterDatacenterId
let _ = (strongSelf.account.postbox.modify { modifier -> Void in
modifier.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: 1, number: "")))
}).start()
}
}
}
return controller
}
private func phoneEntryController(countryCode: Int32, number: String) -> AuthorizationSequencePhoneEntryController {
var currentController: AuthorizationSequencePhoneEntryController?
for c in self.viewControllers {
if let c = c as? AuthorizationSequencePhoneEntryController {
currentController = c
break
}
}
let controller: AuthorizationSequencePhoneEntryController
if let currentController = currentController {
controller = currentController
} else {
controller = AuthorizationSequencePhoneEntryController()
controller.loginWithNumber = { [weak self, weak controller] number in
if let strongSelf = self {
controller?.inProgress = true
let account = strongSelf.account
let sendCode = Api.functions.auth.sendCode(flags: 0, phoneNumber: number, currentNumber: nil, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb")
let signal = account.network.request(sendCode, automaticFloodWait: false)
|> map { result in
return (result, account)
} |> `catch` { error -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in
switch error.errorDescription {
case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"):
let range = error.errorDescription.range(of: "MIGRATE_")!
let updatedMasterDatacenterId = Int32(error.errorDescription.substring(from: range.upperBound))!
let updatedAccount = account.changedMasterDatacenterId(updatedMasterDatacenterId)
return updatedAccount
|> mapToSignalPromotingError { updatedAccount -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in
return updatedAccount.network.request(sendCode, automaticFloodWait: false)
|> map { sentCode in
return (sentCode, updatedAccount)
}
}
case _:
return .fail(error)
}
}
strongSelf.actionDisposable.set(signal.start(next: { [weak self] (result, account) in
if let strongSelf = self {
strongSelf.account = account
let masterDatacenterId = account.masterDatacenterId
let _ = (strongSelf.account.postbox.modify { modifier -> Void in
switch result {
case let .sentCode(_, type, phoneCodeHash, nextType, timeout):
var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: nextType)
}
modifier.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType)))
}
}).start()
}
}, error: { error in
Queue.mainQueue().async {
if let controller = controller {
controller.inProgress = false
var text: String?
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
text = "You have requested authorization code too many times. Please try again later."
} else if error.errorDescription == "PHONE_NUMBER_INVALID" {
text = "The phone number you entered is not valid. Please enter the correct number along with your area code."
}
if let text = text {
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
}
}
}
}))
}
}
}
controller.updateData(countryCode: countryCode, number: number)
return controller
}
private func codeEntryController(number: String, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) -> AuthorizationSequenceCodeEntryController {
var currentController: AuthorizationSequenceCodeEntryController?
for c in self.viewControllers {
if let c = c as? AuthorizationSequenceCodeEntryController {
currentController = c
break
}
}
let controller: AuthorizationSequenceCodeEntryController
if let currentController = currentController {
controller = currentController
} else {
controller = AuthorizationSequenceCodeEntryController()
controller.loginWithCode = { [weak self, weak controller] code in
if let strongSelf = self {
controller?.inProgress = true
let account = strongSelf.account
let masterDatacenterId = account.masterDatacenterId
let signal = account.postbox.modify { modifier -> Signal<Void, String> in
if let state = modifier.getState() as? UnauthorizedAccountState {
switch state.contents {
case let .confirmationCodeEntry(number, _, hash, _, _):
return account.network.request(Api.functions.auth.signIn(phoneNumber: number, phoneCodeHash: hash, phoneCode: code), automaticFloodWait: false) |> map { authorization in
return AuthorizationCodeResult.Authorization(authorization)
} |> `catch` { error -> Signal<AuthorizationCodeResult, String> in
switch (error.errorCode, error.errorDescription) {
case (401, "SESSION_PASSWORD_NEEDED"):
return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
|> mapError { error -> String in
return error.errorDescription
}
|> mapToSignal { result -> Signal<AuthorizationCodeResult, String> in
switch result {
case .noPassword:
return .fail("NO_PASSWORD")
case let .password(_, _, hint, _, _):
return .single(.Password(hint))
}
}
case _:
return .fail(error.errorDescription)
}
}
|> mapToSignal { result -> Signal<Void, String> in
return account.postbox.modify { modifier -> Void in
switch result {
case let .Password(hint):
modifier.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .passwordEntry(hint: hint)))
case let .Authorization(authorization):
switch authorization {
case let .authorization(_, _, user):
let user = TelegramUser(user: user)
let state = AuthorizedAccountState(masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil)
modifier.setState(state)
}
}
} |> mapToSignalPromotingError { result -> Signal<Void, String> in
return .complete()
}
}
default:
break
}
}
return .complete()
}
|> mapError { _ -> String in
return ""
}
|> switchToLatest
strongSelf.actionDisposable.set(signal.start(error: { error in
Queue.mainQueue().async {
if let controller = controller {
controller.inProgress = false
var text: String?
if error.hasPrefix("FLOOD_WAIT") {
text = "You have entered invalid code too many times. Please try again later."
} else if error == "CODE_INVALID" {
text = "Invalid code."
} else {
text = "An error occured.";
}
if let text = text {
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
}
}
}
}))
}
}
}
controller.updateData(number: formatPhoneNumber(number), codeType: type, nextType: nextType, timeout: timeout)
return controller
}
private func passwordEntryController(hint: String) -> AuthorizationSequencePasswordEntryController {
var currentController: AuthorizationSequencePasswordEntryController?
for c in self.viewControllers {
if let c = c as? AuthorizationSequencePasswordEntryController {
currentController = c
break
}
}
let controller: AuthorizationSequencePasswordEntryController
if let currentController = currentController {
controller = currentController
} else {
controller = AuthorizationSequencePasswordEntryController()
controller.loginWithPassword = { [weak self, weak controller] password in
if let strongSelf = self {
controller?.inProgress = true
let account = strongSelf.account
let signal = verifyPassword(account, password: password)
|> `catch` { error -> Signal<Api.auth.Authorization, String> in
return .fail(error.errorDescription)
}
|> mapToSignal { result -> Signal<Void, String> in
return account.postbox.modify { modifier -> Void in
switch result {
case let .authorization(_, _, user):
let user = TelegramUser(user: user)
let state = AuthorizedAccountState(masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil)
modifier.setState(state)
}
}
|> mapToSignalPromotingError { _ -> Signal<Void, String> in
return .complete()
}
}
strongSelf.actionDisposable.set(signal.start(error: { error in
Queue.mainQueue().async {
if let controller = controller {
controller.inProgress = false
var text: String?
if error.hasPrefix("FLOOD_WAIT") {
text = "You have entered invalid password too many times. Please try again later."
} else if error == "PASSWORD_HASH_INVALID" {
text = "Invalid password."
} else {
text = "An error occured.";
}
if let text = text {
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
}
}
}
}))
}
}
}
controller.updateData(hint: hint)
return controller
}
private func updateState(state: Coding?) {
if let state = state as? UnauthorizedAccountState {
switch state.contents {
case .empty:
if let _ = self.viewControllers.last as? AuthorizationSequenceSplashController {
} else {
self.setViewControllers([self.splashController()], animated: !self.viewControllers.isEmpty)
}
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 .passwordEntry(hint):
self.setViewControllers([self.splashController(), self.passwordEntryController(hint: hint)], animated: !self.viewControllers.isEmpty)
}
} else if let _ = state as? AuthorizedAccountState {
self._authorizedAccount.set(accountWithId(self.account.id, appGroupPath: self.account.appGroupPath, logger: .instance(self.account.logger), testingEnvironment: self.account.testingEnvironment) |> mapToSignal { account -> Signal<Account, NoError> in
if case let .right(authorizedAccount) = account {
return .single(authorizedAccount)
} else {
return .complete()
}
})
}
}
}

View File

@ -0,0 +1,499 @@
import Foundation
import Display
import AsyncDisplayKit
private let countryNamesAndCodes: [(String, Int)] = [
("Jamaica", 1876),
("Saint Kitts & Nevis", 1869),
("Trinidad & Tobago", 1868),
("Saint Vincent & the Grenadines", 1784),
("Dominica", 1767),
("Saint Lucia", 1758),
("Sint Maarten", 1721),
("American Samoa", 1684),
("Guam", 1671),
("Northern Mariana Islands", 1670),
("Montserrat", 1664),
("Turks & Caicos Islands", 1649),
("Grenada", 1473),
("Bermuda", 1441),
("Cayman Islands", 1345),
("US Virgin Islands", 1340),
("British Virgin Islands", 1284),
("Antigua & Barbuda", 1268),
("Anguilla", 1264),
("Barbados", 1246),
("Bahamas", 1242),
("Uzbekistan", 998),
("Kyrgyzstan", 996),
("Georgia", 995),
("Azerbaijan", 994),
("Turkmenistan", 993),
("Tajikistan", 992),
("Nepal", 977),
("Mongolia", 976),
("Bhutan", 975),
("Qatar", 974),
("Bahrain", 973),
("Israel", 972),
("United Arab Emirates", 971),
("Palestine", 970),
("Oman", 968),
("Yemen", 967),
("Saudi Arabia", 966),
("Kuwait", 965),
("Iraq", 964),
("Syrian Arab Republic", 963),
("Jordan", 962),
("Lebanon", 961),
("Maldives", 960),
("Taiwan", 886),
("Bangladesh", 880),
("Laos", 856),
("Cambodia", 855),
("Macau", 853),
("Hong Kong", 852),
("North Korea", 850),
("Marshall Islands", 692),
("Micronesia", 691),
("Tokelau", 690),
("French Polynesia", 689),
("Tuvalu", 688),
("New Caledonia", 687),
("Kiribati", 686),
("Samoa", 685),
("Niue", 683),
("Cook Islands", 682),
("Wallis & Futuna", 681),
("Palau", 680),
("Fiji", 679),
("Vanuatu", 678),
("Solomon Islands", 677),
("Tonga", 676),
("Papua New Guinea", 675),
("Nauru", 674),
("Brunei Darussalam", 673),
("Norfolk Island", 672),
("Timor-Leste", 670),
("Bonaire, Sint Eustatius & Saba", 599),
("Curaçao", 599),
("Uruguay", 598),
("Suriname", 597),
("Martinique", 596),
("Paraguay", 595),
("French Guiana", 594),
("Ecuador", 593),
("Guyana", 592),
("Bolivia", 591),
("Guadeloupe", 590),
("Haiti", 509),
("Saint Pierre & Miquelon", 508),
("Panama", 507),
("Costa Rica", 506),
("Nicaragua", 505),
("Honduras", 504),
("El Salvador", 503),
("Guatemala", 502),
("Belize", 501),
("Falkland Islands", 500),
("Liechtenstein", 423),
("Slovakia", 421),
("Czech Republic", 420),
("Macedonia", 389),
("Bosnia & Herzegovina", 387),
("Slovenia", 386),
("Croatia", 385),
("Montenegro", 382),
("Serbia", 381),
("Ukraine", 380),
("San Marino", 378),
("Monaco", 377),
("Andorra", 376),
("Belarus", 375),
("Armenia", 374),
("Moldova", 373),
("Estonia", 372),
("Latvia", 371),
("Lithuania", 370),
("Bulgaria", 359),
("Finland", 358),
("Cyprus", 357),
("Malta", 356),
("Albania", 355),
("Iceland", 354),
("Ireland", 353),
("Luxembourg", 352),
("Portugal", 351),
("Gibraltar", 350),
("Greenland", 299),
("Faroe Islands", 298),
("Aruba", 297),
("Eritrea", 291),
("Saint Helena", 290),
("Comoros", 269),
("Swaziland", 268),
("Botswana", 267),
("Lesotho", 266),
("Malawi", 265),
("Namibia", 264),
("Zimbabwe", 263),
("Réunion", 262),
("Madagascar", 261),
("Zambia", 260),
("Mozambique", 258),
("Burundi", 257),
("Uganda", 256),
("Tanzania", 255),
("Kenya", 254),
("Djibouti", 253),
("Somalia", 252),
("Ethiopia", 251),
("Rwanda", 250),
("Sudan", 249),
("Seychelles", 248),
("Saint Helena", 247),
("Diego Garcia", 246),
("Guinea-Bissau", 245),
("Angola", 244),
("Congo (Dem. Rep.)", 243),
("Congo (Rep.)", 242),
("Gabon", 241),
("Equatorial Guinea", 240),
("São Tomé & Príncipe", 239),
("Cape Verde", 238),
("Cameroon", 237),
("Central African Rep.", 236),
("Chad", 235),
("Nigeria", 234),
("Ghana", 233),
("Sierra Leone", 232),
("Liberia", 231),
("Mauritius", 230),
("Benin", 229),
("Togo", 228),
("Niger", 227),
("Burkina Faso", 226),
("Côte d`Ivoire", 225),
("Guinea", 224),
("Mali", 223),
("Mauritania", 222),
("Senegal", 221),
("Gambia", 220),
("Libya", 218),
("Tunisia", 216),
("Algeria", 213),
("Morocco", 212),
("South Sudan", 211),
("Iran", 98),
("Myanmar", 95),
("Sri Lanka", 94),
("Afghanistan", 93),
("Pakistan", 92),
("India", 91),
("Turkey", 90),
("China", 86),
("Vietnam", 84),
("South Korea", 82),
("Japan", 81),
("Thailand", 66),
("Singapore", 65),
("New Zealand", 64),
("Philippines", 63),
("Indonesia", 62),
("Australia", 61),
("Malaysia", 60),
("Venezuela", 58),
("Colombia", 57),
("Chile", 56),
("Brazil", 55),
("Argentina", 54),
("Cuba", 53),
("Mexico", 52),
("Peru", 51),
("Germany", 49),
("Poland", 48),
("Norway", 47),
("Sweden", 46),
("Denmark", 45),
("United Kingdom", 44),
("Austria", 43),
("Switzerland", 41),
("Romania", 40),
("Italy", 39),
("Hungary", 36),
("Spain", 34),
("France", 33),
("Belgium", 32),
("Netherlands", 31),
("Greece", 30),
("South Africa", 27),
("Egypt", 20),
("Russian Federation", 7),
("Kazakhstan", 7),
("USA", 1),
("Puerto Rico", 1),
("Dominican Rep.", 1),
("Canada", 1)
]
private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let tableView: UITableView
var searchResults: [(String, Int)] = [] {
didSet {
self.tableView.reloadData()
}
}
var itemSelected: (((String, Int)) -> Void)?
init() {
self.tableView = UITableView(frame: CGRect(), style: .plain)
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.tableView)
self.tableView.frame = self.view.bounds
self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.tableView.dataSource = self
self.tableView.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.searchResults.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") {
cell = currentCell
} else {
cell = UITableViewCell()
let label = UILabel()
label.font = Font.medium(17.0)
cell.accessoryView = label
}
cell.textLabel?.text = self.searchResults[indexPath.row].0
if let label = cell.accessoryView as? UILabel {
label.text = "+\(self.searchResults[indexPath.row].1)"
label.sizeToFit()
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.itemSelected?(self.searchResults[indexPath.row])
}
}
private final class InnerCountrySelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate {
private let tableView: UITableView
private let sections: [(String, [(String, Int)])]
private let sectionTitles: [String]
private var searchController: UISearchController!
private var searchResultsController: InnerCoutrySearchResultsController!
var dismiss: (() -> Void)?
var itemSelected: (((String, Int)) -> Void)?
init() {
self.tableView = UITableView(frame: CGRect(), style: .plain)
var sections: [(String, [(String, Int)])] = []
for (name, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in
return lhs.0 < rhs.0
}) {
let title = name.substring(to: name.index(after: name.startIndex)).uppercased()
if sections.isEmpty || sections[sections.count - 1].0 != title {
sections.append((title, []))
}
sections[sections.count - 1].1.append((name, code))
}
self.sections = sections
var sectionTitles = sections.map { $0.0 }
sectionTitles.insert(UITableViewIndexSearch, at: 0)
self.sectionTitles = sectionTitles
super.init(nibName: nil, bundle: nil)
self.title = "Select Country"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed))
self.definesPresentationContext = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.searchResultsController = InnerCoutrySearchResultsController()
self.searchResultsController.itemSelected = { [weak self] item in
self?.itemSelected?(item)
}
self.searchController = UISearchController(searchResultsController: self.searchResultsController)
self.searchController.searchResultsUpdater = self
self.searchController.dimsBackgroundDuringPresentation = true
self.searchController.searchBar.delegate = self
self.view.addSubview(self.tableView)
self.tableView.tableHeaderView = self.searchController.searchBar
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.frame = self.view.bounds
self.view.addSubview(self.tableView)
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].1.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section].0
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.sectionTitles
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
if index == 0 {
return 0
} else {
return max(0, index - 1)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") {
cell = currentCell
} else {
cell = UITableViewCell()
let label = UILabel()
label.font = Font.medium(17.0)
cell.accessoryView = label
}
cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0
if let label = cell.accessoryView as? UILabel {
label.text = "+\(self.sections[indexPath.section].1[indexPath.row].1)"
label.sizeToFit()
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.itemSelected?(self.sections[indexPath.section].1[indexPath.row])
}
func updateSearchResults(for searchController: UISearchController) {
guard let normalizedQuery = searchController.searchBar.text?.lowercased() else {
self.searchResultsController.searchResults = []
return
}
var results: [(String, Int)] = []
for (_, items) in self.sections {
for item in items {
if item.0.lowercased().hasPrefix(normalizedQuery) {
results.append(item)
}
}
}
self.searchResultsController.searchResults = results
}
@objc func cancelPressed() {
self.dismiss?()
}
}
final class AuthorizationSequenceCountrySelectionController: ViewController {
private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode {
return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode
}
private let innerNavigationController: UINavigationController
private let innerController: InnerCountrySelectionController
var completeWithCountryCode: ((Int) -> Void)?
override init(navigationBar: NavigationBar = NavigationBar()) {
self.innerController = InnerCountrySelectionController()
self.innerNavigationController = UINavigationController(rootViewController: self.innerController)
super.init(navigationBar: navigationBar)
self.navigationBar.isHidden = true
self.innerController.dismiss = { [weak self] in
self?.cancelPressed()
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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] _, code in
self?.completeWithCountryCode?(code)
self?.controllerNode.animateOut()
}
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.innerNavigationController.viewWillAppear(false)
self.innerNavigationController.viewDidAppear(false)
self.controllerNode.animateIn()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
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.navigationBar.frame.maxY, transition: transition)
}
private func cancelPressed() {
self.controllerNode.animateOut()
}
}

View File

@ -0,0 +1,31 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode {
var dismiss: (() -> Void)?
override init() {
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
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?()
}
})
}
}

View File

@ -0,0 +1,79 @@
import Foundation
import Display
import AsyncDisplayKit
final class AuthorizationSequencePasswordEntryController: ViewController {
private var controllerNode: AuthorizationSequencePasswordEntryControllerNode {
return self.displayNode as! AuthorizationSequencePasswordEntryControllerNode
}
var loginWithPassword: ((String) -> Void)?
var hint: String?
private let hapticFeedback = HapticFeedback()
var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode())
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
self.controllerNode.inProgress = self.inProgress
}
}
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequencePasswordEntryControllerNode()
self.displayNodeDidLoad()
if let hint = self.hint {
self.controllerNode.updateData(hint: hint)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.activateInput()
}
func updateData(hint: String) {
if self.hint != hint {
self.hint = hint
if self.isNodeLoaded {
self.controllerNode.updateData(hint: hint)
}
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
}
@objc func nextPressed() {
if self.controllerNode.currentPassword.isEmpty {
hapticFeedback.error()
self.controllerNode.animateError()
} else {
self.loginWithPassword?(self.controllerNode.currentPassword)
}
}
}

View File

@ -0,0 +1,174 @@
import Foundation
import AsyncDisplayKit
import Display
final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode {
private let navigationBackgroundNode: ASDisplayNode
private let stripeNode: ASDisplayNode
private let titleNode: ASTextNode
private let currentOptionNode: ASTextNode
private let nextOptionNode: ASTextNode
private let codeField: TextFieldNode
private let codeSeparatorNode: ASDisplayNode
private var layoutArguments: (ContainerViewLayout, CGFloat)?
var currentPassword: String {
return self.codeField.textField.text ?? ""
}
var loginWithCode: ((String) -> Void)?
var requestNextOption: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.codeField.alpha = self.inProgress ? 0.6 : 1.0
}
}
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: "Your Password", font: Font.light(30.0), textColor: UIColor.black)
self.currentOptionNode = ASTextNode()
self.currentOptionNode.isLayerBacked = true
self.currentOptionNode.displaysAsynchronously = false
self.currentOptionNode.attributedText = NSAttributedString(string: "Two-step verification enabled.\nYour account is protected with an\nadditional password.", font: Font.regular(16.0), textColor: UIColor.black, paragraphAlignment: .center)
self.nextOptionNode = ASTextNode()
self.nextOptionNode.isLayerBacked = true
self.nextOptionNode.displaysAsynchronously = false
self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center)
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(20.0)
self.codeField.textField.textAlignment = .natural
self.codeField.textField.isSecureTextEntry = true
self.codeField.textField.returnKeyType = .done
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
self.addSubnode(self.navigationBackgroundNode)
self.addSubnode(self.stripeNode)
self.addSubnode(self.codeSeparatorNode)
self.addSubnode(self.codeField)
self.addSubnode(self.titleNode)
self.addSubnode(self.currentOptionNode)
self.addSubnode(self.nextOptionNode)
}
func updateData(hint: String) {
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.layoutArguments = (layout, navigationBarHeight)
let insets = layout.insets(options: [.statusBar, .input])
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 Password", font: Font.light(30.0), textColor: UIColor.black)
} else {
self.titleNode.attributedText = NSAttributedString(string: "Your Password", font: Font.regular(20.0), textColor: UIColor.black)
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let additionalTitleSpacing: CGFloat
if titleSize.width > layout.size.width - 160.0 {
additionalTitleSpacing = 44.0
} else {
additionalTitleSpacing = 0.0
}
let minimalTitleSpacing: CGFloat = 10.0
let maxTitleSpacing: CGFloat = 22.0
let inputFieldsHeight: CGFloat = 60.0
let minimalNoticeSpacing: CGFloat = 11.0
let maxNoticeSpacing: CGFloat = 35.0
let noticeSize = self.currentOptionNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
let minimalTermsOfServiceSpacing: CGFloat = 6.0
let maxTermsOfServiceSpacing: CGFloat = 20.0
let termsOfServiceSize = self.nextOptionNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let minTrailingSpacing: CGFloat = 10.0
let inputHeight = inputFieldsHeight
let essentialHeight = additionalTitleSpacing + titleSize.height + minimalTitleSpacing + inputHeight + minimalNoticeSpacing + noticeSize.height
let additionalHeight = minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing
let navigationHeight: CGFloat
if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight {
navigationHeight = min(floor(availableHeight * 0.3), availableHeight - inputFieldsHeight)
} else {
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)
} else {
titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height)
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize))
let codeFieldFrame = CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 3.0), size: CGSize(width: layout.size.width - 44.0, height: 60.0))
transition.updateFrame(node: self.codeField, frame: codeFieldFrame)
transition.updateFrame(node: self.codeSeparatorNode, frame: CGRect(origin: CGPoint(x: 22.0, y: navigationHeight + 60.0), size: CGSize(width: layout.size.width - 44.0, height: UIScreenPixel)))
let additionalAvailableHeight = max(1.0, availableHeight - codeFieldFrame.maxY)
let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height)
let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let noticeSpacing: CGFloat
let termsOfServiceSpacing: CGFloat
if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing {
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0)
} else {
noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing)
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
}
let currentOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: codeFieldFrame.maxY + noticeSpacing), size: noticeSize)
let nextOptionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: currentOptionFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize)
transition.updateFrame(node: self.currentOptionNode, frame: currentOptionFrame)
transition.updateFrame(node: self.nextOptionNode, frame: nextOptionFrame)
}
func activateInput() {
self.codeField.textField.becomeFirstResponder()
}
func animateError() {
self.codeField.layer.addShakeAnimation()
}
@objc func passwordFieldTextChanged(_ textField: UITextField) {
}
}

View File

@ -0,0 +1,95 @@
import Foundation
import Display
import AsyncDisplayKit
final class AuthorizationSequencePhoneEntryController: ViewController {
private var controllerNode: AuthorizationSequencePhoneEntryControllerNode {
return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode
}
private var currentData: (Int32, String)?
var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode())
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
self.controllerNode.inProgress = self.inProgress
}
}
var loginWithNumber: ((String) -> Void)?
private let hapticFeedback = HapticFeedback()
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateData(countryCode: Int32, number: String) {
if self.currentData == nil || self.currentData! != (countryCode, number) {
self.currentData = (countryCode, number)
if self.isNodeLoaded {
self.controllerNode.codeAndNumber = (countryCode, number)
}
}
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequencePhoneEntryControllerNode()
self.displayNodeDidLoad()
self.controllerNode.selectCountryCode = { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController()
controller.completeWithCountryCode = { code in
if let strongSelf = self, let currentData = strongSelf.currentData {
strongSelf.updateData(countryCode: Int32(code), number: currentData.1)
strongSelf.controllerNode.activateInput()
}
}
strongSelf.controllerNode.view.endEditing(true)
strongSelf.present(controller, in: .window)
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.controllerNode.activateInput()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.activateInput()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
}
@objc func nextPressed() {
let (code, number) = self.controllerNode.codeAndNumber
if code != nil && !number.isEmpty {
self.loginWithNumber?(self.controllerNode.currentNumber)
} else {
hapticFeedback.error()
self.controllerNode.animateError()
}
}
}

View File

@ -0,0 +1,488 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
let countryCodeToName: [Int: String] = [
1876: "Jamaica",
1869: "Saint Kitts & Nevis",
1868: "Trinidad & Tobago",
1784: "Saint Vincent & the Grenadines",
1767: "Dominica",
1758: "Saint Lucia",
1721: "Sint Maarten",
1684: "American Samoa",
1671: "Guam",
1670: "Northern Mariana Islands",
1664: "Montserrat",
1649: "Turks & Caicos Islands",
1473: "Grenada",
1441: "Bermuda",
1345: "Cayman Islands",
1340: "US Virgin Islands",
1284: "British Virgin Islands",
1268: "Antigua & Barbuda",
1264: "Anguilla",
1246: "Barbados",
1242: "Bahamas",
998: "Uzbekistan",
996: "Kyrgyzstan",
995: "Georgia",
994: "Azerbaijan",
993: "Turkmenistan",
992: "Tajikistan",
977: "Nepal",
976: "Mongolia",
975: "Bhutan",
974: "Qatar",
973: "Bahrain",
972: "Israel",
971: "United Arab Emirates",
970: "Palestine",
968: "Oman",
967: "Yemen",
966: "Saudi Arabia",
965: "Kuwait",
964: "Iraq",
963: "Syrian Arab Republic",
962: "Jordan",
961: "Lebanon",
960: "Maldives",
886: "Taiwan",
880: "Bangladesh",
856: "Laos",
855: "Cambodia",
853: "Macau",
852: "Hong Kong",
850: "North Korea",
692: "Marshall Islands",
691: "Micronesia",
690: "Tokelau",
689: "French Polynesia",
688: "Tuvalu",
687: "New Caledonia",
686: "Kiribati",
685: "Samoa",
683: "Niue",
682: "Cook Islands",
681: "Wallis & Futuna",
680: "Palau",
679: "Fiji",
678: "Vanuatu",
677: "Solomon Islands",
676: "Tonga",
675: "Papua New Guinea",
674: "Nauru",
673: "Brunei Darussalam",
672: "Norfolk Island",
670: "Timor-Leste",
599: "Bonaire, Sint Eustatius & Saba",
//599: "Curaçao",
598: "Uruguay",
597: "Suriname",
596: "Martinique",
595: "Paraguay",
594: "French Guiana",
593: "Ecuador",
592: "Guyana",
591: "Bolivia",
590: "Guadeloupe",
509: "Haiti",
508: "Saint Pierre & Miquelon",
507: "Panama",
506: "Costa Rica",
505: "Nicaragua",
504: "Honduras",
503: "El Salvador",
502: "Guatemala",
501: "Belize",
500: "Falkland Islands",
423: "Liechtenstein",
421: "Slovakia",
420: "Czech Republic",
389: "Macedonia",
387: "Bosnia & Herzegovina",
386: "Slovenia",
385: "Croatia",
382: "Montenegro",
381: "Serbia",
380: "Ukraine",
378: "San Marino",
377: "Monaco",
376: "Andorra",
375: "Belarus",
374: "Armenia",
373: "Moldova",
372: "Estonia",
371: "Latvia",
370: "Lithuania",
359: "Bulgaria",
358: "Finland",
357: "Cyprus",
356: "Malta",
355: "Albania",
354: "Iceland",
353: "Ireland",
352: "Luxembourg",
351: "Portugal",
350: "Gibraltar",
299: "Greenland",
298: "Faroe Islands",
297: "Aruba",
291: "Eritrea",
290: "Saint Helena",
269: "Comoros",
268: "Swaziland",
267: "Botswana",
266: "Lesotho",
265: "Malawi",
264: "Namibia",
263: "Zimbabwe",
262: "Réunion",
261: "Madagascar",
260: "Zambia",
258: "Mozambique",
257: "Burundi",
256: "Uganda",
255: "Tanzania",
254: "Kenya",
253: "Djibouti",
252: "Somalia",
251: "Ethiopia",
250: "Rwanda",
249: "Sudan",
248: "Seychelles",
247: "Saint Helena",
246: "Diego Garcia",
245: "Guinea-Bissau",
244: "Angola",
243: "Congo (Dem. Rep.)",
242: "Congo (Rep.)",
241: "Gabon",
240: "Equatorial Guinea",
239: "São Tomé & Príncipe",
238: "Cape Verde",
237: "Cameroon",
236: "Central African Rep.",
235: "Chad",
234: "Nigeria",
233: "Ghana",
232: "Sierra Leone",
231: "Liberia",
230: "Mauritius",
229: "Benin",
228: "Togo",
227: "Niger",
226: "Burkina Faso",
225: "Côte d`Ivoire",
224: "Guinea",
223: "Mali",
222: "Mauritania",
221: "Senegal",
220: "Gambia",
218: "Libya",
216: "Tunisia",
213: "Algeria",
212: "Morocco",
211: "South Sudan",
98: "Iran",
95: "Myanmar",
94: "Sri Lanka",
93: "Afghanistan",
92: "Pakistan",
91: "India",
90: "Turkey",
86: "China",
84: "Vietnam",
82: "South Korea",
81: "Japan",
66: "Thailand",
65: "Singapore",
64: "New Zealand",
63: "Philippines",
62: "Indonesia",
61: "Australia",
60: "Malaysia",
58: "Venezuela",
57: "Colombia",
56: "Chile",
55: "Brazil",
54: "Argentina",
53: "Cuba",
52: "Mexico",
51: "Peru",
49: "Germany",
48: "Poland",
47: "Norway",
46: "Sweden",
45: "Denmark",
44: "United Kingdom",
43: "Austria",
41: "Switzerland",
40: "Romania",
39: "Italy",
36: "Hungary",
34: "Spain",
33: "France",
32: "Belgium",
31: "Netherlands",
30: "Greece",
27: "South Africa",
20: "Egypt",
7: "Russian Federation",
// 7: "Kazakhstan",
1: "USA",
// 1: "Puerto Rico",
// 1: "Dominican Rep.",
// 1: "Canada"
]
private let countryButtonBackground = generateImage(CGSize(width: 61.0, height: 67.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 1)
private let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0xbcbbc1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 61, topCapHeight: 2)
private let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 84, topCapHeight: 2)
final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let navigationBackgroundNode: ASDisplayNode
private let stripeNode: ASDisplayNode
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let termsOfServiceNode: ASTextNode
private let countryButton: ASButtonNode
private let phoneBackground: ASImageNode
private let phoneInputNode: PhoneInputNode
var currentNumber: String {
return self.phoneInputNode.number
}
var codeAndNumber: (Int32?, String) {
get {
return self.phoneInputNode.codeAndNumber
} set(value) {
self.phoneInputNode.codeAndNumber = value
}
}
var selectCountryCode: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.phoneInputNode.enableEditing = !self.inProgress
self.phoneInputNode.alpha = self.inProgress ? 0.6 : 1.0
self.countryButton.isEnabled = !self.inProgress
}
}
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: "Your Phone", font: Font.light(30.0), textColor: UIColor.black)
self.noticeNode = ASTextNode()
self.noticeNode.isLayerBacked = true
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center)
self.termsOfServiceNode = ASTextNode()
self.termsOfServiceNode.isLayerBacked = true
self.termsOfServiceNode.displaysAsynchronously = false
let termsString = NSMutableAttributedString()
termsString.append(NSAttributedString(string: "By signing up,\nyou agree to the ", font: Font.regular(16.0), textColor: UIColor.black))
termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(0x007ee5)))
termsString.append(NSAttributedString(string: ".", font: Font.regular(16.0), textColor: UIColor.black))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
termsString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSMakeRange(0, termsString.length))
self.termsOfServiceNode.attributedText = termsString
self.countryButton = ASButtonNode()
self.countryButton.setBackgroundImage(countryButtonBackground, for: [])
self.countryButton.setBackgroundImage(countryButtonHighlightedBackground, for: .highlighted)
self.phoneBackground = ASImageNode()
self.phoneBackground.image = phoneInputBackground
self.phoneBackground.displaysAsynchronously = false
self.phoneBackground.displayWithoutProcessing = true
self.phoneBackground.isLayerBacked = true
self.phoneInputNode = PhoneInputNode()
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
self.addSubnode(self.navigationBackgroundNode)
self.addSubnode(self.stripeNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.termsOfServiceNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.phoneBackground)
self.addSubnode(self.countryButton)
self.addSubnode(self.phoneInputNode)
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
self.phoneInputNode.countryCodeUpdated = { [weak self] code in
if let strongSelf = self {
if let code = Int(code), let countryName = countryCodeToName[code] {
strongSelf.countryButton.setTitle(countryName, with: Font.regular(20.0), with: .black, for: [])
} else {
strongSelf.countryButton.setTitle("Select Country", with: Font.regular(20.0), with: .black, for: [])
}
}
}
self.phoneInputNode.number = "+1"
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let insets = layout.insets(options: [.statusBar, .input])
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 Phone", font: Font.light(40.0), textColor: UIColor.black)
} else {
self.titleNode.attributedText = NSAttributedString(string: "Your Phone", font: Font.light(30.0), textColor: UIColor.black)
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let minimalTitleSpacing: CGFloat = 10.0
let maxTitleSpacing: CGFloat = 28.0
let countryButtonHeight: CGFloat = 57.0
let inputFieldsHeight: CGFloat = 57.0
let minimalNoticeSpacing: CGFloat = 11.0
let maxNoticeSpacing: CGFloat = 35.0
let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0, height: CGFloat.greatestFiniteMagnitude))
let minimalTermsOfServiceSpacing: CGFloat = 6.0
let maxTermsOfServiceSpacing: CGFloat = 20.0
let termsOfServiceSize = self.termsOfServiceNode.measure(CGSize(width: layout.size.width, height: CGFloat.greatestFiniteMagnitude))
let minTrailingSpacing: CGFloat = 10.0
let inputHeight = countryButtonHeight + inputFieldsHeight
let essentialHeight = titleSize.height + minimalTitleSpacing + inputHeight
let additionalHeight = minimalNoticeSpacing + noticeSize.height + minimalTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing
let navigationHeight: CGFloat
if essentialHeight + additionalHeight > availableHeight || availableHeight * 0.66 - inputHeight < additionalHeight {
transition.updateAlpha(node: self.noticeNode, alpha: 0.0)
transition.updateAlpha(node: self.termsOfServiceNode, alpha: 0.0)
navigationHeight = min(floor(availableHeight * 0.3), availableHeight - countryButtonHeight - inputFieldsHeight)
} else {
transition.updateAlpha(node: self.noticeNode, alpha: 1.0)
transition.updateAlpha(node: self.termsOfServiceNode, alpha: 1.0)
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)
} else {
titleOffset = max(navigationHeight * 0.5, navigationHeight - maxTitleSpacing - titleSize.height)
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: titleOffset), size: titleSize))
transition.updateFrame(node: self.countryButton, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: 67.0)))
transition.updateFrame(node: self.phoneBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + 57.0), size: CGSize(width: layout.size.width, height: 57.0)))
let countryCodeFrame = CGRect(origin: CGPoint(x: 18.0, y: navigationHeight + 58.0), size: CGSize(width: 60.0, height: 57.0))
let numberFrame = CGRect(origin: CGPoint(x: 96.0, y: navigationHeight + 58.0), size: CGSize(width: layout.size.width - 96.0 - 8.0, height: 57.0))
let phoneInputFrame = countryCodeFrame.union(numberFrame)
transition.updateFrame(node: self.phoneInputNode, frame: phoneInputFrame)
transition.updateFrame(node: self.phoneInputNode.countryCodeField, frame: countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
transition.updateFrame(node: self.phoneInputNode.numberField, frame: numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
let additionalAvailableHeight = max(1.0, availableHeight - phoneInputFrame.maxY)
let additionalAvailableSpacing = max(1.0, additionalAvailableHeight - noticeSize.height - termsOfServiceSize.height)
let noticeSpacingFactor = maxNoticeSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let termsOfServiceSpacingFactor = maxTermsOfServiceSpacing / (maxNoticeSpacing + maxTermsOfServiceSpacing + minTrailingSpacing)
let noticeSpacing: CGFloat
let termsOfServiceSpacing: CGFloat
if additionalAvailableHeight <= maxNoticeSpacing + noticeSize.height + maxTermsOfServiceSpacing + termsOfServiceSize.height + minTrailingSpacing {
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
noticeSpacing = floor((additionalAvailableHeight - termsOfServiceSpacing - noticeSize.height - termsOfServiceSize.height) / 2.0)
} else {
noticeSpacing = min(floor(noticeSpacingFactor * additionalAvailableSpacing), maxNoticeSpacing)
termsOfServiceSpacing = min(floor(termsOfServiceSpacingFactor * additionalAvailableSpacing), maxTermsOfServiceSpacing)
}
let noticeFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - noticeSize.width) / 2.0), y: phoneInputFrame.maxY + noticeSpacing), size: noticeSize)
let termsOfServiceFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - termsOfServiceSize.width) / 2.0), y: noticeFrame.maxY + termsOfServiceSpacing), size: termsOfServiceSize)
transition.updateFrame(node: self.noticeNode, frame: noticeFrame)
transition.updateFrame(node: self.termsOfServiceNode, frame: termsOfServiceFrame)
}
func activateInput() {
self.phoneInputNode.numberField.textField.becomeFirstResponder()
}
func animateError() {
self.phoneInputNode.countryCodeField.layer.addShakeAnimation()
self.phoneInputNode.numberField.layer.addShakeAnimation()
}
@objc func countryPressed() {
self.selectCountryCode?()
}
}

View File

@ -0,0 +1,80 @@
import Foundation
import Display
import AsyncDisplayKit
import TelegramUIPrivateModule
final class AuthorizationSequenceSplashController: ViewController {
private var controllerNode: AuthorizationSequenceSplashControllerNode {
return self.displayNode as! AuthorizationSequenceSplashControllerNode
}
private let controller = RMIntroViewController()
var nextPressed: (() -> Void)?
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.isHidden = true
self.controller.startMessaging = { [weak self] in
self?.nextPressed?()
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = AuthorizationSequenceSplashControllerNode()
self.displayNodeDidLoad()
}
private func addControllerIfNeeded() {
if !controller.isViewLoaded {
self.displayNode.view.addSubview(controller.view)
controller.view.frame = self.displayNode.bounds;
controller.viewDidAppear(false)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated);
controller.viewWillAppear(false)
self.addControllerIfNeeded()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
controller.viewDidAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
controller.viewWillDisappear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
controller.viewDidDisappear(animated)
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
self.addControllerIfNeeded()
if case .immediate = transition {
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
} else {
UIView.animate(withDuration: 0.3, animations: {
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
})
}
}
}

View File

@ -0,0 +1,17 @@
import Foundation
import AsyncDisplayKit
import Display
final class AuthorizationSequenceSplashControllerNode: ASDisplayNode {
override init() {
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
}

View File

@ -165,7 +165,7 @@ enum ChannelInfoEntry: PeerInfoEntry {
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
switch self {
case let .info(peer, cachedData):
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, cachedData: cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: nil, updatingName: nil), sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: nil, cachedData: cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: nil, updatingName: nil), sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
})
case let .about(text):

View File

@ -64,6 +64,9 @@ public class ChatController: TelegramController {
private var chatUnreadCountDisposable: Disposable?
private var peerInputActivitiesDisposable: Disposable?
private var recentlyUsedInlineBotsValue: [Peer] = []
private var recentlyUsedInlineBotsDisposable: Disposable?
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) {
self.account = account
self.peerId = peerId
@ -264,7 +267,7 @@ public class ChatController: TelegramController {
})
}
})
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start()
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start()
}
}, requestMessageActionCallback: { [weak self] messageId, data, isGame in
if let strongSelf = self {
@ -334,6 +337,7 @@ public class ChatController: TelegramController {
}
}, shareAccountContact: { [weak self] in
if let strongSelf = self {
}
}, sendBotCommand: { [weak self] messageId, command in
if let strongSelf = self {
@ -560,6 +564,7 @@ public class ChatController: TelegramController {
self.resolveUrlDisposable?.dispose()
self.chatUnreadCountDisposable?.dispose()
self.peerInputActivitiesDisposable?.dispose()
self.recentlyUsedInlineBotsDisposable?.dispose()
}
var chatDisplayNode: ChatControllerNode {
@ -698,7 +703,7 @@ public class ChatController: TelegramController {
let legacyController = LegacyController(legacyController: navigationController, presentation: .custom)
var presentOverlayController: ((UIViewController) -> (() -> Void))?
let controller = legacyAttachmentMenu(parentController: legacyController, presentOverlayController: { controller in
let controller = legacyAttachmentMenu(parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, presentOverlayController: { controller in
if let presentOverlayController = presentOverlayController {
return presentOverlayController(controller)
} else {
@ -715,8 +720,44 @@ public class ChatController: TelegramController {
}
}, openFileGallery: {
self?.presentMediaPicker(fileMode: true)
}, openMap: {
}, openContacts: {
if let strongSelf = self {
let contactsController = ContactSelectionController(account: strongSelf.account, title: "Select Contact")
strongSelf.present(contactsController, in: .window, 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)
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [message]).start()
}
}))
}
}))
}
}, 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: "@" + addressName + " ")) }).updatedInputMode({ _ in
return .text
})
})
}
})
controller.applicationInterface = legacyController.applicationInterface
controller.didDismiss = { [weak legacyController] _ in
@ -727,7 +768,7 @@ public class ChatController: TelegramController {
controller.present(in: emptyController, sourceView: nil, animated: true)
presentOverlayController = { [weak legacyController] controller in
if let strongSelf = self, let legacyController = legacyController {
if let legacyController = legacyController {
let childController = LegacyController(legacyController: controller, presentation: .custom)
legacyController.present(childController, in: .window)
return { [weak childController] in
@ -741,42 +782,6 @@ public class ChatController: TelegramController {
return
}
let controller = ChatMediaActionSheetController()
controller.photo = { [weak strongSelf] asset in
if let strongSelf = strongSelf {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight))
let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0))
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier)
if false {
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)])
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: media, replyToMessageId: nil)]).start()
} else {
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "image/jpeg", size: 0, attributes: [.FileName(fileName: "image.jpeg"), .ImageSize(size: scaledSize)])
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: media, replyToMessageId: nil)]).start()
}
}
}
controller.files = { [weak strongSelf] in
if let strongSelf = strongSelf {
}
}
controller.location = { [weak strongSelf] in
if let strongSelf = strongSelf {
let mapInputController = MapInputController()
strongSelf.present(mapInputController, in: .window)
}
}
controller.contacts = { [weak strongSelf] in
if let strongSelf = strongSelf {
}
}
strongSelf.present(controller, in: .window)
}
}
@ -809,7 +814,7 @@ public class ChatController: TelegramController {
}, deleteSelectedMessages: { [weak self] in
if let strongSelf = self {
if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start()
let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start()
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
}
@ -828,14 +833,13 @@ public class ChatController: TelegramController {
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) })
strongController.dismiss()
} else {
(strongSelf.account.postbox.modify({ modifier -> Void in
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)
}
return currentState
})
}) |> deliverOnMainQueue).start(completed: {
if let strongSelf = self {
@ -913,7 +917,7 @@ public class ChatController: TelegramController {
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]).start()
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]).start()
}
}
}, sendBotStart: { [weak self] payload in
@ -935,12 +939,23 @@ public class ChatController: TelegramController {
if let peer = strongSelf.presentationInterfaceState.peer as? TelegramSecretChat {
let controller = ChatSecretAutoremoveTimerActionSheetController(currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in
if let strongSelf = self {
setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start()
let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start()
}
})
strongSelf.present(controller, in: .window)
}
}
}, sendSticker: { [weak self] file in
if let strongSelf = self {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) }
})
}
})
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start()
}
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get()))
self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in
@ -1017,6 +1032,10 @@ public class ChatController: TelegramController {
self.chatDisplayNode.historyNode.canReadHistory.set(true)
self.chatDisplayNode.loadInputPanels()
self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in
self?.recentlyUsedInlineBotsValue = peers
})
}
override public func viewWillDisappear(_ animated: Bool) {
@ -1254,7 +1273,7 @@ public class ChatController: TelegramController {
})
}
})
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }).start()
let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }).start()
}
}))
}

View File

@ -318,7 +318,7 @@ class ChatControllerNode: ASDisplayNode {
immediatelyLayoutAccessoryPanelAndAnimateAppearance = true
}
} else if let accessoryPanelNode = self.accessoryPanelNode {
dismissedAccessoryPanelNode = self.accessoryPanelNode
dismissedAccessoryPanelNode = accessoryPanelNode
self.accessoryPanelNode = nil
}
@ -328,7 +328,7 @@ class ChatControllerNode: ASDisplayNode {
dismissedInputContextPanelNode = self.inputContextPanelNode
self.inputContextPanelNode = inputContextPanelNode
self.insertSubnode(inputContextPanelNode, aboveSubnode: self.navigateToLatestButton)
self.addSubnode(inputContextPanelNode)
immediatelyLayoutInputContextPanelAndAnimateAppearance = true
}

View File

@ -23,6 +23,11 @@ class ChatDocumentGalleryItem: GalleryItem {
if let file = media as? TelegramMediaFile {
node.setFile(account: account, file: file)
break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file {
node.setFile(account: account, file: file)
break
}
}
}

View File

@ -321,7 +321,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
return dict
}
if apply {
applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start()
let _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start()
}
}
}

View File

@ -26,6 +26,14 @@ class ChatImageGalleryItem: GalleryItem {
} else if let file = media as? TelegramMediaFile, file.mimeType.hasPrefix("image/") {
node.setFile(account: account, file: file)
break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let image = content.image {
node.setImage(account: account, image: image)
break
} else if let file = content.file, file.mimeType.hasPrefix("image/") {
node.setFile(account: account, file: file)
break
}
}
}

View File

@ -7,6 +7,18 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
}
switch inputQueryResult {
case let .stickers(results):
if !results.isEmpty {
if let currentPanel = currentPanel as? HorizontalStickersChatContextPanelNode {
currentPanel.updateResults(results.map({ $0.file }))
return currentPanel
} else {
let panel = HorizontalStickersChatContextPanelNode(account: account)
panel.interfaceInteraction = interfaceInteraction
panel.updateResults(results.map({ $0.file }))
return panel
}
}
case let .hashtags(results):
if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode {
currentPanel.updateResults(results)

View File

@ -14,10 +14,11 @@ struct PossibleContextQueryTypes: OptionSet {
self.rawValue = rawValue
}
static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 0))
static let mention = PossibleContextQueryTypes(rawValue: (1 << 1))
static let command = PossibleContextQueryTypes(rawValue: (1 << 2))
static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 3))
static let emoji = PossibleContextQueryTypes(rawValue: (1 << 0))
static let hashtag = PossibleContextQueryTypes(rawValue: (1 << 1))
static let mention = PossibleContextQueryTypes(rawValue: (1 << 2))
static let command = PossibleContextQueryTypes(rawValue: (1 << 3))
static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4))
}
private func makeScalar(_ c: Character) -> Character {
@ -81,6 +82,10 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) ->
var possibleQueryRange: Range<String.Index>?
if inputText.isSingleEmoji {
return (inputText.startIndex ..< inputText.endIndex, [.emoji], nil)
}
var possibleTypes = PossibleContextQueryTypes([.command, .mention])
var definedType = false
@ -128,7 +133,9 @@ func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfa
let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState
if let (possibleQueryRange, possibleTypes, additionalStringRange) = textInputStateContextQueryRangeAndType(inputState) {
let query = inputState.inputText.substring(with: possibleQueryRange)
if possibleTypes == [.hashtag] {
if possibleTypes == [.emoji] {
return .emoji(query)
} else if possibleTypes == [.hashtag] {
return .hashtag(query)
} else if possibleTypes == [.mention] {
return .mention(query)

View File

@ -9,6 +9,23 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
return nil
} else {
switch inputQuery {
case let .emoji(query):
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete()
if let currentQuery = currentQuery {
switch currentQuery {
case .emoji:
break
default:
signal = .single({ _ in return nil })
}
}
let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = searchStickers(postbox: account.postbox, query: query)
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in
return .stickers(stickers)
}
}
return (inputQuery, signal |> then(stickers))
case let .hashtag(query):
/*var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete()
if let currentQuery = currentQuery {

View File

@ -113,6 +113,26 @@ public class ChatListController: TelegramController {
self?.activateSearch()
}
self.chatListDisplayNode.chatListNode.deletePeerChat = { [weak self] peerId in
if let strongSelf = self {
let actionSheet = ActionSheetController()
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
let _ = removePeerChat(postbox: strongSelf.account.postbox, peerId: peerId).start()
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window)
}
}
self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))

View File

@ -40,10 +40,10 @@ class ChatListItem: ListViewItem {
async {
let node = ChatListItemNode()
node.setupItem(item: self)
let (first, last, firstWithHeader) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
let (nodeLayout, apply) = node.asyncLayout()(self, width, first, last, firstWithHeader)
let (nodeLayout, apply) = node.asyncLayout()(self, width, first, last, firstWithHeader, nextIsPinned)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
@ -63,13 +63,13 @@ class ChatListItem: ListViewItem {
node.setupItem(item: self)
let layout = node.asyncLayout()
async {
let (first, last, firstWithHeader) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
var animated = true
if case .None = animation {
animated = false
}
let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader)
let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader, nextIsPinned)
Queue.mainQueue().async {
completion(nodeLayout, {
apply(animated)
@ -88,7 +88,7 @@ class ChatListItem: ListViewItem {
}
}
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool) {
var first = false
var last = false
var firstWithHeader = false
@ -104,11 +104,15 @@ class ChatListItem: ListViewItem {
first = true
firstWithHeader = item.header != nil
}
if let _ = nextItem {
var nextIsPinned = false
if let nextItem = nextItem as? ChatListItem {
if nextItem.index.pinningIndex != nil {
nextIsPinned = true
}
} else {
last = true
}
return (first, last, firstWithHeader)
return (first, last, firstWithHeader, nextIsPinned)
}
}
@ -199,6 +203,8 @@ private let peerMutedIcon = UIImage(bundleImageName: "Chat List/PeerMutedIcon")?
private let separatorHeight = 1.0 / UIScreen.main.scale
private let pinnedBackgroundColor = UIColor(0xf7f7f7)
class ChatListItemNode: ItemListRevealOptionsItemNode {
var item: ChatListItem?
@ -218,7 +224,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var editableControlNode: ItemListEditableControlNode?
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool)?
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool)?
override var canBeSelected: Bool {
if self.editableControlNode != nil {
@ -318,8 +324,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (first, last, firstWithHeader) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(item as! ChatListItem, width, first, last, firstWithHeader)
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(item as! ChatListItem, width, first, last, firstWithHeader, nextIsPinned)
apply(false)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
@ -348,7 +354,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
strongSelf.contentNode.backgroundColor = UIColor.white
if let item = strongSelf.layoutParams?.0, item.index.pinningIndex != nil {
strongSelf.contentNode.backgroundColor = pinnedBackgroundColor
} else {
strongSelf.contentNode.backgroundColor = UIColor.white
}
strongSelf.contentNode.isOpaque = true
strongSelf.contentNode.displaysAsynchronously = true
}
@ -357,7 +368,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
self.contentNode.backgroundColor = UIColor.white
if let item = self.layoutParams?.0, item.index.pinningIndex != nil {
self.contentNode.backgroundColor = pinnedBackgroundColor
} else {
self.contentNode.backgroundColor = UIColor.white
}
self.contentNode.isOpaque = true
self.contentNode.displaysAsynchronously = true
}
@ -365,14 +380,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
func asyncLayout() -> (_ item: ChatListItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
func asyncLayout() -> (_ item: ChatListItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNode.asyncLayout(self.textNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
return { item, width, first, last, firstWithHeader in
return { item, width, first, last, firstWithHeader, nextIsPinned in
let account = item.account
let message = item.message
let combinedReadState = item.combinedReadState
@ -536,7 +551,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, first, last, firstWithHeader)
strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned)
let revealOffset = strongSelf.revealOffset
@ -633,9 +648,29 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size)
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: editingOffset + 78.0 + contentRect.origin.x, y: 68.0 - separatorHeight), size: CGSize(width: width - 78.0 - editingOffset, height: separatorHeight)))
let separatorInset: CGFloat
if !nextIsPinned && item.index.pinningIndex != nil {
separatorInset = 0.0
} else {
separatorInset = editingOffset + 78.0 + contentRect.origin.x
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: width - separatorInset, height: separatorHeight)))
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if item.index.pinningIndex != nil {
strongSelf.backgroundNode.backgroundColor = pinnedBackgroundColor
if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(pinnedBackgroundColor) {
strongSelf.contentNode.backgroundColor = pinnedBackgroundColor
updateContentNode = true
}
} else {
strongSelf.backgroundNode.backgroundColor = UIColor.white
if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(UIColor.white) {
strongSelf.contentNode.backgroundColor = UIColor.white
updateContentNode = true
}
}
let topNegativeInset: CGFloat = 0.0
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
@ -672,7 +707,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
override public func header() -> ListViewItemHeader? {
if let (item, _, _, _) = self.layoutParams {
if let (item, _, _, _, _) = self.layoutParams {
return item.header
} else {
return nil
@ -721,15 +756,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item {
switch option.key {
case RevealOptionKey.pin.rawValue:
break
item.interaction.setPeerPinned(item.index.messageIndex.id.peerId, true)
case RevealOptionKey.unpin.rawValue:
break
item.interaction.setPeerPinned(item.index.messageIndex.id.peerId, false)
case RevealOptionKey.mute.rawValue:
break
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, true)
case RevealOptionKey.unmute.rawValue:
break
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false)
case RevealOptionKey.delete.rawValue:
item.interaction
item.interaction.deletePeer(item.index.messageIndex.id.peerId)
default:
break
}

View File

@ -148,6 +148,7 @@ final class ChatListNode: ListView {
var peerSelected: ((PeerId) -> Void)?
var activateSearch: (() -> Void)?
var deletePeerChat: ((PeerId) -> Void)?
private let viewProcessingQueue = Queue()
private var chatListView: ChatListNodeView?
@ -187,10 +188,12 @@ final class ChatListNode: ListView {
}
}
}
}, setPeerPinned: { _ in
}, setPeerMuted: { _ in
}, deletePeer: { peerId in
let _ = removePeerChat(postbox: account.postbox, peerId: peerId).start()
}, setPeerPinned: { peerId, _ in
let _ = togglePeerChatPinned(postbox: account.postbox, peerId: peerId).start()
}, setPeerMuted: { peerId, _ in
let _ = togglePeerMuted(account: account, peerId: peerId).start()
}, deletePeer: { [weak self] peerId in
self?.deletePeerChat?(peerId)
})
let viewProcessingQueue = self.viewProcessingQueue

View File

@ -0,0 +1,134 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
class ChatListRecentPeersListItem: ListViewItem {
let account: Account
let peers: [Peer]
let peerSelected: (Peer) -> Void
let header: ListViewItemHeader?
init(account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void) {
self.account = account
self.peers = peers
self.peerSelected = peerSelected
self.header = nil
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
async {
let node = ChatListRecentPeersListItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, width, nextItem != nil)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
if let node = node as? ChatListRecentPeersListItemNode {
Queue.mainQueue().async {
let layout = node.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, width, nextItem != nil)
Queue.mainQueue().async {
completion(nodeLayout, {
apply().1()
})
}
}
}
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
class ChatListRecentPeersListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var peersNode: ChatListSearchRecentPeersNode?
private var item: ChatListRecentPeersListItem?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = .white
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, width, nextItem == nil)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ width: CGFloat, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, () -> Void)) {
return { [weak self] item, width, last in
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 130.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
return (nodeLayout, { [weak self] in
return (nil, {
if let strongSelf = self {
strongSelf.item = item
let peersNode: ChatListSearchRecentPeersNode
if let currentPeersNode = strongSelf.peersNode {
peersNode = currentPeersNode
} else {
peersNode = ChatListSearchRecentPeersNode(account: item.account, peerSelected: { peer in
self?.item?.peerSelected(peer)
})
strongSelf.peersNode = peersNode
strongSelf.addSubnode(peersNode)
}
let separatorHeight = UIScreenPixel
peersNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = true
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func header() -> ListViewItemHeader? {
if let item = self.item {
return item.header
} else {
return nil
}
}
}

View File

@ -5,6 +5,104 @@ import SwiftSignalKit
import Postbox
import TelegramCore
enum ChatListRecentEntryStableId: Hashable {
case topPeers
case peerId(PeerId)
static func ==(lhs: ChatListRecentEntryStableId, rhs: ChatListRecentEntryStableId) -> Bool {
switch lhs {
case .topPeers:
if case .topPeers = rhs {
return true
} else {
return false
}
case let .peerId(peerId):
if case .peerId(peerId) = rhs {
return true
} else {
return false
}
}
}
var hashValue: Int {
switch self {
case .topPeers:
return 0
case let .peerId(peerId):
return peerId.hashValue
}
}
}
enum ChatListRecentEntry: Comparable, Identifiable {
case topPeers([Peer])
case peer(index: Int, peer: Peer)
var stableId: ChatListRecentEntryStableId {
switch self {
case .topPeers:
return .topPeers
case let .peer(_, peer):
return .peerId(peer.id)
}
}
static func ==(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool {
switch lhs {
case let .topPeers(lhsPeers):
if case let .topPeers(rhsPeers) = rhs {
if lhsPeers.count != rhsPeers.count {
return false
}
for i in 0 ..< lhsPeers.count {
if !lhsPeers[i].isEqual(rhsPeers[i]) {
return false
}
}
return true
} else {
return false
}
case let .peer(lhsIndex, lhsPeer):
if case let .peer(rhsIndex, rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex {
return true
} else {
return false
}
}
}
static func <(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool {
switch lhs {
case .topPeers:
return true
case let .peer(lhsIndex, _):
switch rhs {
case .topPeers:
return false
case let .peer(rhsIndex, _):
return lhsIndex < rhsIndex
}
}
}
func item(account: Account, peerSelected: @escaping (Peer) -> Void) -> ListViewItem {
switch self {
case let .topPeers(peers):
return ChatListRecentPeersListItem(account: account, peers: peers, peerSelected: { peer in
peerSelected(peer)
})
case let .peer(_, peer):
return ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers), action: { _ in
peerSelected(peer)
})
}
}
}
enum ChatListSearchEntryStableId: Hashable {
case localPeerId(PeerId)
case globalPeerId(PeerId)
@ -133,6 +231,12 @@ enum ChatListSearchEntry: Comparable, Identifiable {
}
}
struct ChatListSearchContainerRecentTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
struct ChatListSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
@ -140,6 +244,16 @@ struct ChatListSearchContainerTransition {
let displayingResults: Bool
}
func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, peerSelected: @escaping (Peer) -> 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, peerSelected: peerSelected), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) }
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
}
func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, enableHeaders: Bool, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
@ -154,11 +268,16 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
private let account: Account
private let openMessage: (Peer, MessageId) -> Void
private let recentPeersNode: ChatListSearchRecentPeersNode
//private let recentPeersNode: ChatListSearchRecentPeersNode
private let recentListNode: ListView
private let listNode: ListView
private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = []
private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = []
private var hasValidLayout = false
private let recentDisposable = MetaDisposable()
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
@ -166,18 +285,21 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
self.account = account
self.openMessage = openMessage
self.recentPeersNode = ChatListSearchRecentPeersNode(account: account, peerSelected: openPeer)
//self.recentPeersNode = ChatListSearchRecentPeersNode(account: account, peerSelected: openPeer)
self.recentListNode = ListView()
self.listNode = ListView()
super.init()
self.backgroundColor = UIColor.white
self.addSubnode(self.recentPeersNode)
//self.addSubnode(self.recentPeersNode)
self.addSubnode(self.recentListNode)
self.addSubnode(self.listNode)
self.listNode.isHidden = true
let foundItems = searchQuery.get()
|> mapToSignal { query -> Signal<[ChatListSearchEntry]?, NoError> in
if let query = query, !query.isEmpty {
@ -224,6 +346,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
let interaction = ChatListNodeInteraction(activateSearch: {
}, peerSelected: { [weak self] peer in
openPeer(peer)
let _ = addRecentlySearchedPeer(postbox: account.postbox, peerId: peer.id).start()
self?.listNode.clearHighlightAnimated(true)
}, messageSelected: { [weak self] message in
if let peer = message.peers[message.id.peerId] {
@ -236,6 +359,29 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
}, deletePeer: { _ in
})
let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil)
let recentItemsTransition = recentlySearchedPeers(postbox: account.postbox)
|> mapToSignal { [weak self] peers -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in
var entries: [ChatListRecentEntry] = []
entries.append(.topPeers([]))
for i in 0 ..< peers.count {
entries.append(.peer(index: i, peer: peers[i]))
}
let previousEntries = previousRecentItems.swap(entries)
let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, peerSelected: { peer in
self?.recentListNode.clearHighlightAnimated(true)
openPeer(peer)
})
return .single((transition, previousEntries == nil))
}
self.recentDisposable.set((recentItemsTransition |> deliverOnMainQueue).start(next: { [weak self] (transition, firstTime) in
if let strongSelf = self {
strongSelf.enqueueRecentTransition(transition, firstTime: firstTime)
}
}))
self.searchDisposable.set((foundItems
|> deliverOnMainQueue).start(next: { [weak self] entries in
if let strongSelf = self {
@ -249,6 +395,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
}
deinit {
self.recentDisposable.dispose()
self.searchDisposable.dispose()
}
@ -260,6 +407,31 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
}
}
private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) {
enqueuedRecentTransitions.append((transition, firstTime))
if self.hasValidLayout {
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func dequeueRecentTransition() {
if let (transition, firstTime) = self.enqueuedRecentTransitions.first {
self.enqueuedRecentTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousResourceLoading)
if firstTime {
} else {
}
self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
}
private func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) {
enqueuedTransitions.append((transition, firstTime))
@ -286,7 +458,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
if let strongSelf = self {
if displayingResults != !strongSelf.listNode.isHidden {
strongSelf.listNode.isHidden = !displayingResults
strongSelf.recentPeersNode.isHidden = displayingResults
strongSelf.recentListNode.isHidden = displayingResults
}
}
})
@ -296,9 +468,9 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let recentPeersSize = self.recentPeersNode.measure(CGSize(width: layout.size.width, height: CGFloat.infinity))
self.recentPeersNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: recentPeersSize)
self.recentPeersNode.layout()
//let recentPeersSize = self.recentPeersNode.measure(CGSize(width: layout.size.width, height: CGFloat.infinity))
//self.recentPeersNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: recentPeersSize)
//self.recentPeersNode.layout()
var duration: Double = 0.0
var curve: UInt = 0
@ -323,11 +495,17 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
listViewCurve = .Default
}
self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
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: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !hasValidLayout {
hasValidLayout = true
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}

View File

@ -6,6 +6,9 @@ import Display
import SwiftSignalKit
private let searchBarFont = Font.regular(15.0)
private let pinnedBackgroundColor = UIColor(0xf7f7f7)
private let regularSearchBackgroundColor = UIColor(0xededed)
private let pinnedSearchBackgroundColor = UIColor(0xe5e5e5)
class ChatListSearchItem: ListViewItem {
let selectable: Bool = false
@ -24,14 +27,20 @@ class ChatListSearchItem: ListViewItem {
node.placeholder = self.placeholder
let makeLayout = node.asyncLayout()
let (layout, apply) = makeLayout(width)
var nextIsPinned = false
if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil {
nextIsPinned = true
}
let (layout, apply) = makeLayout(width, nextIsPinned)
node.contentSize = layout.contentSize
node.insets = layout.insets
node.activate = self.activate
completion(node, {
return (nil, apply)
return (nil, {
apply(false)
})
})
}
}
@ -41,10 +50,14 @@ class ChatListSearchItem: ListViewItem {
Queue.mainQueue().async {
let layout = node.asyncLayout()
async {
let (nodeLayout, apply) = layout(width)
var nextIsPinned = false
if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil {
nextIsPinned = true
}
let (nodeLayout, apply) = layout(width, nextIsPinned)
Queue.mainQueue().async {
completion(nodeLayout, {
apply()
apply(animation.isAnimated)
})
}
}
@ -73,27 +86,40 @@ class ChatListSearchItemNode: ListViewItemNode {
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let makeLayout = self.asyncLayout()
let (layout, apply) = makeLayout(width)
apply()
var nextIsPinned = false
if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil {
nextIsPinned = true
}
let (layout, apply) = makeLayout(width, nextIsPinned)
apply(false)
self.contentSize = layout.contentSize
self.insets = layout.insets
}
func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) {
func asyncLayout() -> (_ width: CGFloat, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { width in
let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "Search", font: searchBarFont, textColor: UIColor(0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude))
return { width, nextIsPinned in
let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "Search", font: searchBarFont, textColor: UIColor(0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), nextIsPinned ? pinnedSearchBackgroundColor : regularSearchBackgroundColor)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 44.0 + 4.0), insets: UIEdgeInsets())
return (layout, { [weak self] in
return (layout, { [weak self] animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: width - 16.0, height: 28.0))
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: width - 16.0, height: 28.0))
transition.updateBackgroundColor(node: strongSelf, color: nextIsPinned ? pinnedBackgroundColor : UIColor.white)
}
})
}

View File

@ -4,6 +4,7 @@ import Display
enum ChatListSearchItemHeaderType: Int32 {
case localPeers
case globalPeers
case recentPeers
case messages
}
@ -43,6 +44,8 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
self.sectionHeaderNode.title = "GLOBAL SEARCH"
case .messages:
self.sectionHeaderNode.title = "MESSAGES"
case .recentPeers:
self.sectionHeaderNode.title = "RECENT"
}
self.addSubnode(self.sectionHeaderNode)

View File

@ -51,15 +51,19 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr
}
stationaryItems = .indices(indices)
case let .navigate(index):
for i in 0 ..< toEntries.count {
if toEntries[i].index >= index {
var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up
if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index {
directionHint = .down
if let index = index {
for i in 0 ..< toEntries.count {
if toEntries[i].index >= index {
var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up
if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index {
directionHint = .down
}
scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true)
break
}
scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true)
break
}
} else if !toEntries.isEmpty {
scrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true)
}
}
@ -77,8 +81,11 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem)
}
private func chatMediaInputPanelEntries(view: ItemCollectionsView) -> [ChatMediaInputPanelEntry] {
private func chatMediaInputPanelEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?) -> [ChatMediaInputPanelEntry] {
var entries: [ChatMediaInputPanelEntry] = []
if let recentStickers = recentStickers, !recentStickers.items.isEmpty {
entries.append(.recentPacks)
}
var index = 0
for (_, info, item) in view.collectionInfos {
if let info = info as? StickerPackCollectionInfo {
@ -89,7 +96,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView) -> [ChatMedia
return entries
}
private func chatMediaInputGridEntries(view: ItemCollectionsView) -> [ChatMediaInputGridEntry] {
private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?) -> [ChatMediaInputGridEntry] {
var entries: [ChatMediaInputGridEntry] = []
var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
@ -99,6 +106,17 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView) -> [ChatMediaI
}
}
if let recentStickers = recentStickers, !recentStickers.items.isEmpty {
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudRecentStickers, id: 0), accessHash: 0, title: "FREQUENTLY USED", shortName: "", hash: 0)
for i in 0 ..< min(20, recentStickers.items.count) {
if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id {
let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id)
let stickerItem = StickerPackItem(index: index, file: file, indexKeys: [])
entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo))
}
}
}
for entry in view.entries {
if let item = entry.item as? StickerPackItem {
entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId]))
@ -109,8 +127,8 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView) -> [ChatMediaI
private enum StickerPacksCollectionPosition: Equatable {
case initial
case scroll(aroundIndex: ItemCollectionViewEntryIndex)
case navigate(index: ItemCollectionViewEntryIndex)
case scroll(aroundIndex: ItemCollectionViewEntryIndex?)
case navigate(index: ItemCollectionViewEntryIndex?)
static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool {
switch lhs {
@ -120,8 +138,8 @@ private enum StickerPacksCollectionPosition: Equatable {
} else {
return false
}
case let .scroll(aroundIndex):
if case .scroll(aroundIndex) = rhs {
case let .scroll(lhsAroundIndex):
if case let .scroll(rhsAroundIndex) = rhs, lhsAroundIndex == rhsAroundIndex {
return true
} else {
return false
@ -135,7 +153,7 @@ private enum StickerPacksCollectionPosition: Equatable {
private enum StickerPacksCollectionUpdate {
case generic
case scroll
case navigate(ItemCollectionViewEntryIndex)
case navigate(ItemCollectionViewEntryIndex?)
}
final class ChatMediaInputNodeInteraction {
@ -148,6 +166,18 @@ final class ChatMediaInputNodeInteraction {
}
}
private func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> StickerPacksCollectionPosition {
switch position {
case let .scroll(index):
if let index = index, index.collectionId.namespace == Namespaces.ItemCollection.CloudRecentStickers {
return .scroll(aroundIndex: nil)
}
default:
break
}
return position
}
private let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0
private let defaultLandscapePanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 194.0 : 194.0
@ -190,14 +220,18 @@ final class ChatMediaInputNode: ChatInputNode {
self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in
if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) {
var index: Int32 = 0
for (id, _, _) in currentView.collectionInfos {
if id.namespace == collectionId.namespace {
if id == collectionId {
let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id)
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex)))
break
if collectionId.namespace == Namespaces.ItemCollection.CloudRecentStickers {
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil)))
} else {
for (id, _, _) in currentView.collectionInfos {
if id.namespace == collectionId.namespace {
if id == collectionId {
let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id)
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex)))
break
}
index += 1
}
index += 1
}
}
}
@ -216,13 +250,13 @@ final class ChatMediaInputNode: ChatInputNode {
|> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in
switch position {
case .initial:
return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50)
return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50)
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
return (view, .generic)
}
case let .scroll(aroundIndex):
var firstTime = true
return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140)
return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140)
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
let update: StickerPacksCollectionUpdate
if firstTime {
@ -235,7 +269,7 @@ final class ChatMediaInputNode: ChatInputNode {
}
case let .navigate(index):
var firstTime = true
return account.postbox.itemCollectionsView(namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 140)
return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 140)
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
let update: StickerPacksCollectionUpdate
if firstTime {
@ -255,8 +289,15 @@ final class ChatMediaInputNode: ChatInputNode {
let transitions = itemCollectionsView
|> map { (view, update) -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
let panelEntries = chatMediaInputPanelEntries(view: view)
let gridEntries = chatMediaInputGridEntries(view: view)
var recentStickers: OrderedItemListView?
for orderedView in view.orderedItemListsViews {
if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers {
recentStickers = orderedView
break
}
}
let panelEntries = chatMediaInputPanelEntries(view: view, recentStickers: recentStickers)
let gridEntries = chatMediaInputGridEntries(view: view, recentStickers: recentStickers)
let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries))
return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty)
}
@ -281,7 +322,6 @@ final class ChatMediaInputNode: ChatInputNode {
if let collectionId = topVisibleCollectionId {
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId {
strongSelf.inputNodeInteraction.highlightedItemCollectionId = collectionId
var selectedItemNode: ChatMediaInputStickerPackItemNode?
var ensuredNodeVisible = false
var firstVisibleCollectionId: ItemCollectionId?
strongSelf.listView.forEachItemNode { itemNode in
@ -294,6 +334,12 @@ final class ChatMediaInputNode: ChatInputNode {
strongSelf.listView.ensureItemNodeVisible(itemNode)
ensuredNodeVisible = true
}
} else if let itemNode = itemNode as? ChatMediaInputRecentStickerPacksItemNode {
itemNode.updateIsHighlighted()
if itemNode.currentCollectionId == collectionId {
strongSelf.listView.ensureItemNodeVisible(itemNode)
ensuredNodeVisible = true
}
}
}
if let currentView = strongSelf.currentView, let firstVisibleCollectionId = firstVisibleCollectionId, !ensuredNodeVisible {
@ -309,13 +355,13 @@ final class ChatMediaInputNode: ChatInputNode {
if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom {
if topIndex <= 10 && currentView.lower != nil {
let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index)
let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index))
if strongSelf.currentStickerPacksCollectionPosition != position {
strongSelf.currentStickerPacksCollectionPosition = position
strongSelf.itemCollectionsViewPosition.set(.single(position))
}
} else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil {
let position: StickerPacksCollectionPosition = .scroll(aroundIndex: (bottomItem as! ChatMediaInputStickerGridItem).index)
let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (bottomItem as! ChatMediaInputStickerGridItem).index))
if strongSelf.currentStickerPacksCollectionPosition != position {
strongSelf.currentStickerPacksCollectionPosition = position
strongSelf.itemCollectionsViewPosition.set(.single(position))

View File

@ -4,68 +4,95 @@ import SwiftSignalKit
import Display
enum ChatMediaInputPanelEntryStableId: Hashable {
case recentPacks
case stickerPack(Int64)
static func ==(lhs: ChatMediaInputPanelEntryStableId, rhs: ChatMediaInputPanelEntryStableId) -> Bool {
switch lhs {
case let .stickerPack(id):
if case .stickerPack(id) = rhs {
return true
} else {
return false
}
case .recentPacks:
if case .recentPacks = rhs {
return true
} else {
return false
}
case let .stickerPack(id):
if case .stickerPack(id) = rhs {
return true
} else {
return false
}
}
}
var hashValue: Int {
switch self {
case let .stickerPack(id):
return id.hashValue
case .recentPacks:
return 0
case let .stickerPack(id):
return id.hashValue
}
}
}
enum ChatMediaInputPanelEntry: Comparable, Identifiable {
case recentPacks
case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?)
var stableId: ChatMediaInputPanelEntryStableId {
switch self {
case let .stickerPack(_, info, _):
return .stickerPack(info.id.id)
case .recentPacks:
return .recentPacks
case let .stickerPack(_, info, _):
return .stickerPack(info.id.id)
}
}
static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool {
switch lhs {
case let .stickerPack(index, info, topItem):
if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem {
return true
} else {
return false
}
case .recentPacks:
if case .recentPacks = rhs {
return true
} else {
return false
}
case let .stickerPack(index, info, topItem):
if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem {
return true
} else {
return false
}
}
}
static func <(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool {
switch lhs {
case let .stickerPack(lhsIndex, lhsInfo, _):
switch rhs {
case let .stickerPack(rhsIndex, rhsInfo, _):
if lhsIndex == rhsIndex {
return lhsInfo.id.id < rhsInfo.id.id
} else {
return lhsIndex < rhsIndex
case .recentPacks:
return true
case let .stickerPack(lhsIndex, lhsInfo, _):
switch rhs {
case .recentPacks:
return false
case let .stickerPack(rhsIndex, rhsInfo, _):
if lhsIndex == rhsIndex {
return lhsInfo.id.id < rhsInfo.id.id
} else {
return lhsIndex < rhsIndex
}
}
}
}
}
func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem {
switch self {
case let .stickerPack(index, info, topItem):
return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, selected: {
inputNodeInteraction.navigateToCollectionId(info.id)
})
case .recentPacks:
return ChatMediaInputRecentStickerPacksItem(inputNodeInteraction: inputNodeInteraction, selected: {
let collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudRecentStickers, id: 0)
inputNodeInteraction.navigateToCollectionId(collectionId)
})
case let .stickerPack(index, info, topItem):
return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, selected: {
inputNodeInteraction.navigateToCollectionId(info.id)
})
}
}
}

View File

@ -0,0 +1,109 @@
import Foundation
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import Postbox
private let iconImage = generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0x9099A2).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()
})
final class ChatMediaInputRecentStickerPacksItem: ListViewItem {
let inputNodeInteraction: ChatMediaInputNodeInteraction
let selectedItem: () -> Void
var selectable: Bool {
return true
}
init(inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) {
self.inputNodeInteraction = inputNodeInteraction
self.selectedItem = selected
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
async {
let node = ChatMediaInputRecentStickerPacksItemNode()
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.inputNodeInteraction = self.inputNodeInteraction
completion(node, {
return (nil, {})
})
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {
})
}
func selected(listView: ListView) {
self.selectedItem()
}
}
private let boundingSize = CGSize(width: 41.0, height: 41.0)
private let boundingImageSize = CGSize(width: 30.0, height: 30.0)
private let highlightSize = CGSize(width: 35.0, height: 35.0)
private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2))
final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode {
private let imageNode: ASImageNode
private let highlightNode: ASImageNode
var currentCollectionId: ItemCollectionId?
var inputNodeInteraction: ChatMediaInputNodeInteraction?
init() {
self.highlightNode = ASImageNode()
self.highlightNode.isLayerBacked = true
self.highlightNode.image = highlightBackground
self.highlightNode.isHidden = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
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)
self.imageNode.image = iconImage
self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.highlightNode)
self.addSubnode(self.imageNode)
self.currentCollectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudRecentStickers, id: 0)
let imageSize = CGSize(width: 26.0, height: 26.0)
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
}
deinit {
}
func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) {
self.currentCollectionId = collectionId
self.updateIsHighlighted()
}
func updateIsHighlighted() {
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
}
}
}

View File

@ -87,6 +87,17 @@ final class ChatMediaInputStickerGridItem: GridItem {
node.selected = self.selected
return node
}
func update(node: GridItemNode) {
guard let node = node as? ChatMediaInputStickerGridItemNode else {
assertionFailure()
return
}
node.interfaceInteraction = self.interfaceInteraction
node.inputNodeInteraction = self.inputNodeInteraction
node.setup(account: self.account, stickerItem: self.stickerItem)
node.selected = self.selected
}
}
final class ChatMediaInputStickerGridItemNode: GridItemNode {

View File

@ -74,15 +74,15 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
@objc func imageTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let file = self.media as? TelegramMediaFile, let message = self.message, (file.isVideo || file.isAnimated || file.mimeType.hasPrefix("video/")) && !message.containsSecretMedia {
/*if let file = self.media as? TelegramMediaFile, let message = self.message, (file.isVideo || file.isAnimated || file.mimeType.hasPrefix("video/")) && !message.containsSecretMedia {
self.activateLocalContent()
} else {
} else {*/
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
self.activateLocalContent()
} else {
self.progressPressed()
}
}
//}
}
}

View File

@ -32,6 +32,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
private let statusNode: ChatMessageDateAndStatusNode
private var item: ChatMessageItem?
private var webPage: TelegramMediaWebpage?
private var image: TelegramMediaImage?
@ -319,6 +320,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
return (adjustedBoundingSize, { [weak self] animation in
if let strongSelf = self {
strongSelf.item = item
var hasAnimation = true
if case .None = animation {
hasAnimation = false
@ -366,7 +369,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.contentImageNode = contentImageNode
strongSelf.addSubnode(contentImageNode)
contentImageNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
if let strongSelf = strongSelf, let item = strongSelf.item {
strongSelf.controllerInteraction?.openMessage(item.message.id)
}
}
@ -384,7 +387,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.contentFileNode = contentFileNode
strongSelf.addSubnode(contentFileNode)
contentFileNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
if let strongSelf = strongSelf, let item = strongSelf.item {
strongSelf.controllerInteraction?.openMessage(item.message.id)
}
}
@ -440,4 +443,46 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
return .none
}
override func updateHiddenMedia(_ media: [Media]?) {
var currentMedia: Media?
if let webPage = self.webPage {
if case let .Loaded(content) = webPage.content {
if let image = content.image {
currentMedia = image
} else if let file = content.file {
currentMedia = file
}
}
}
if let currentMedia = currentMedia {
if let media = media {
var found = false
for m in media {
if currentMedia.isEqual(m) {
found = true
break
}
}
if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = found
}
} else if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = false
}
}
}
override func transitionNode(media: Media) -> ASDisplayNode? {
if let webPage = self.webPage {
if case let .Loaded(content) = webPage.content {
if let image = content.image, image.isEqual(media) {
return self.contentImageNode
} else if let file = content.file, file.isEqual(media) {
return self.contentImageNode
}
}
}
return nil
}
}

View File

@ -32,9 +32,10 @@ final class ChatPanelInterfaceInteraction {
let beginAudioRecording: () -> Void
let finishAudioRecording: (Bool) -> Void
let setupMessageAutoremoveTimeout: () -> Void
let sendSticker: (TelegramMediaFile) -> Void
let statuses: ChatPanelInterfaceInteractionStatuses?
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> 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, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> 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, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
self.setupReplyMessage = setupReplyMessage
self.setupEditMessage = setupEditMessage
self.beginMessageSelection = beginMessageSelection
@ -53,6 +54,7 @@ final class ChatPanelInterfaceInteraction {
self.beginAudioRecording = beginAudioRecording
self.finishAudioRecording = finishAudioRecording
self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout
self.sendSticker = sendSticker
self.statuses = statuses
}
}

View File

@ -3,6 +3,7 @@ import Postbox
import TelegramCore
enum ChatPresentationInputQuery: Equatable {
case emoji(String)
case hashtag(String)
case mention(String)
case command(String)
@ -10,6 +11,12 @@ enum ChatPresentationInputQuery: Equatable {
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
@ -39,6 +46,7 @@ enum ChatPresentationInputQuery: Equatable {
}
enum ChatPresentationInputQueryResult: Equatable {
case stickers([StickerPackItem])
case hashtags([String])
case mentions([Peer])
case commands([PeerCommand])
@ -46,6 +54,12 @@ enum ChatPresentationInputQueryResult: Equatable {
static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool {
switch lhs {
case let .stickers(lhsItems):
if case let .stickers(rhsItems) = rhs, lhsItems == rhsItems {
return true
} else {
return false
}
case let .hashtags(lhsResults):
if case let .hashtags(rhsResults) = rhs {
return lhsResults == rhsResults

View File

@ -23,6 +23,11 @@ class ChatVideoGalleryItem: GalleryItem {
if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) {
node.setFile(account: account, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia)
break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file, (file.isVideo || file.mimeType.hasPrefix("video/")) {
node.setFile(account: account, file: file, loopVideo: file.isAnimated || self.message.containsSecretMedia)
break
}
}
}

View File

@ -81,6 +81,13 @@ public class ComposeController: ViewController {
}
}
self.contactsNode.openCreateNewSecretChat = { [weak self] in
if let strongSelf = self {
let controller = ContactSelectionController(account: strongSelf.account, title: "New Secret Chat")
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
}
}
self.displayNodeDidLoad()
}

View File

@ -233,7 +233,7 @@ private extension PeerIndexNameRepresentation {
}
}
private func contactListNodeEntries(view: ContactPeersView, presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?) -> [ContactListNodeEntry] {
private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?) -> [ContactListNodeEntry] {
var entries: [ContactListNodeEntry] = []
var orderedPeers: [Peer]
@ -241,14 +241,15 @@ private func contactListNodeEntries(view: ContactPeersView, presentation: Contac
switch presentation {
case let .orderedByPresence(displayVCard):
entries.append(.search)
if displayVCard {
if let peer = view.accountPeer {
if let peer = accountPeer {
entries.append(.vcard(peer))
}
}
orderedPeers = view.peers.sorted(by: { lhs, rhs in
let lhsPresence = view.peerPresences[lhs.id]
let rhsPresence = view.peerPresences[rhs.id]
orderedPeers = peers.sorted(by: { lhs, rhs in
let lhsPresence = presences[lhs.id]
let rhsPresence = presences[rhs.id]
if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence {
if lhsPresence.status < rhsPresence.status {
return false
@ -262,9 +263,8 @@ private func contactListNodeEntries(view: ContactPeersView, presentation: Contac
}
return lhs.id < rhs.id
})
entries.append(.search)
case let .natural(displaySearch, options):
orderedPeers = view.peers.sorted(by: { lhs, rhs in
orderedPeers = peers.sorted(by: { lhs, rhs in
let result = lhs.indexName.isLessThan(other: rhs.indexName)
if result == .orderedSame {
return lhs.id < rhs.id
@ -302,6 +302,8 @@ private func contactListNodeEntries(view: ContactPeersView, presentation: Contac
for i in 0 ..< options.count {
entries.append(.option(i, options[i]))
}
case .search:
orderedPeers = peers
}
var removeIndices: [Int] = []
@ -330,7 +332,7 @@ private func contactListNodeEntries(view: ContactPeersView, presentation: Contac
} else {
selection = .none
}
entries.append(.peer(i, orderedPeers[i], view.peerPresences[orderedPeers[i].id], headers[orderedPeers[i].id], selection))
entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], headers[orderedPeers[i].id], selection))
}
return entries
}
@ -366,6 +368,7 @@ struct ContactListAdditionalOption: Equatable {
enum ContactListPresentation {
case orderedByPresence(displayVCard: Bool)
case natural(displaySearch: Bool, options: [ContactListAdditionalOption])
case search(Signal<String, NoError>)
}
struct ContactListNodeGroupSelectionState: Equatable {
@ -400,7 +403,6 @@ struct ContactListNodeGroupSelectionState: Equatable {
final class ContactListNode: ASDisplayNode {
private let account: Account
private let presentation: ContactListPresentation
let listNode: ListView
@ -440,7 +442,6 @@ final class ContactListNode: ASDisplayNode {
init(account: Account, presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState? = nil) {
self.account = account
self.presentation = presentation
self.listNode = ListView()
@ -463,33 +464,54 @@ final class ContactListNode: ASDisplayNode {
let account = self.account
var firstTime: Int32 = 1
let selectionStateSignal = self.selectionStatePromise.get()
let transition = self.enableUpdatesPromise.get()
|> mapToSignal { enableUpdates -> Signal<ContactsListNodeTransition, NoError> in
if enableUpdates {
return combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId), selectionStateSignal)
|> mapToQueue { view, selectionState -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
let entries = contactListNodeEntries(view: view, presentation: presentation, selectionState: selectionState)
let previous = previousEntries.swap(entries)
let animated: Bool
if let previous = previous {
animated = (entries.count - previous.count) < 20
} else {
animated = false
let transition: Signal<ContactsListNodeTransition, NoError>
if case let .search(query) = presentation {
transition = query
|> mapToSignal { query in
return combineLatest(account.postbox.searchContacts(query: query), selectionStateSignal)
|> mapToQueue { peers, selectionState -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState)
let previous = previousEntries.swap(entries)
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false))
}
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
return signal |> runOn(Queue.mainQueue())
} else {
return signal |> runOn(processingQueue)
}
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: animated))
}
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
return signal |> runOn(Queue.mainQueue())
} else {
return signal |> runOn(processingQueue)
}
}
} else {
return .never()
}
} |> deliverOnMainQueue
} else {
transition = self.enableUpdatesPromise.get()
|> mapToSignal { enableUpdates -> Signal<ContactsListNodeTransition, NoError> in
if enableUpdates {
return combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId), selectionStateSignal)
|> mapToQueue { view, selectionState -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState)
let previous = previousEntries.swap(entries)
let animated: Bool
if let previous = previous {
animated = (entries.count - previous.count) < 20
} else {
animated = false
}
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: animated))
}
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
return signal |> runOn(Queue.mainQueue())
} else {
return signal |> runOn(processingQueue)
}
}
} else {
return .never()
}
} |> deliverOnMainQueue
}
self.disposable.set(transition.start(next: { [weak self] transition in
self?.enqueueTransition(transition)
}))

View File

@ -68,12 +68,13 @@ public class ContactMultiselectionController: ViewController {
self.displayNode = ContactMultiselectionControllerNode(account: self.account)
self._ready.set(self.contactsNode.contactListNode.ready)
self.contactsNode.contactListNode.openPeer = { [weak self] peer in
self.contactsNode.openPeer = { [weak self] peer in
if let strongSelf = self {
var updatedCount: Int?
var addedToken: EditableTokenListToken?
var removedTokenId: AnyHashable?
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in
if let state = state {
let updatedState = state.withToggledPeerId(peer.id)
@ -83,11 +84,17 @@ public class ContactMultiselectionController: ViewController {
addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle)
}
updatedCount = updatedState.selectedPeerIndices.count
selectionState = updatedState
return updatedState
} else {
return nil
}
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
searchResultsNode.updateSelectionState { _ in
return selectionState
}
}
if let updatedCount = updatedCount {
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
@ -105,6 +112,45 @@ public class ContactMultiselectionController: ViewController {
}
}
self.contactsNode.removeSelectedPeer = { [weak self] peerId in
if let strongSelf = self {
var updatedCount: Int?
var removedTokenId: AnyHashable?
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactsNode.contactListNode.updateSelectionState { state in
if let state = state {
let updatedState = state.withToggledPeerId(peerId)
if updatedState.selectedPeerIndices[peerId] == nil {
removedTokenId = peerId
}
updatedCount = updatedState.selectedPeerIndices.count
selectionState = updatedState
return updatedState
} else {
return nil
}
}
if let searchResultsNode = strongSelf.contactsNode.searchResultsNode {
searchResultsNode.updateSelectionState { _ in
return selectionState
}
}
if let updatedCount = updatedCount {
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0
strongSelf.titleView.title = CounterContollerTitle(title: "New Group", counter: "\(updatedCount)/5000")
}
if let removedTokenId = removedTokenId {
strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in
return token.id != removedTokenId
}
}
strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring))
}
}
self.displayNodeDidLoad()
}

View File

@ -3,21 +3,43 @@ import AsyncDisplayKit
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
private struct SearchResultEntry: Identifiable {
let index: Int
let peer: Peer
var stableId: Int64 {
return self.peer.id.toInt64()
}
static func ==(lhs: SearchResultEntry, rhs: SearchResultEntry) -> Bool {
return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer)
}
static func <(lhs: SearchResultEntry, rhs: SearchResultEntry) -> Bool {
return lhs.index < rhs.index
}
}
final class ContactMultiselectionControllerNode: ASDisplayNode {
let contactListNode: ContactListNode
let tokenListNode: EditableTokenListNode
var searchResultsNode: ContactListNode?
private let account: Account
private var searchDisplayController: SearchDisplayController?
private var containerLayout: (ContainerViewLayout, CGFloat)?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((PeerId) -> Void)?
var openPeer: ((Peer) -> Void)?
var removeSelectedPeer: ((PeerId) -> Void)?
var editableTokens: [EditableTokenListToken] = []
private let searchResultsReadyDisposable = MetaDisposable()
init(account: Account) {
self.account = account
self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: false, options: []), selectionState: ContactListNodeGroupSelectionState())
@ -31,6 +53,61 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.addSubnode(self.contactListNode)
self.addSubnode(self.tokenListNode)
self.contactListNode.openPeer = { [weak self] peer in
self?.openPeer?(peer)
}
let searchText = ValuePromise<String>()
self.tokenListNode.deleteToken = { [weak self] id in
self?.removeSelectedPeer?(id as! PeerId)
}
self.tokenListNode.textUpdated = { [weak self] text in
if let strongSelf = self {
searchText.set(text)
if text.isEmpty {
if let searchResultsNode = strongSelf.searchResultsNode {
searchResultsNode.removeFromSupernode()
strongSelf.searchResultsNode = nil
}
} else {
if strongSelf.searchResultsNode == nil {
var selectionState: ContactListNodeGroupSelectionState?
strongSelf.contactListNode.updateSelectionState { state in
selectionState = state
return state
}
let searchResultsNode = ContactListNode(account: account, presentation: ContactListPresentation.search(searchText.get()), selectionState: selectionState)
searchResultsNode.openPeer = { peer in
self?.tokenListNode.setText("")
self?.openPeer?(peer)
}
strongSelf.searchResultsNode = searchResultsNode
searchResultsNode.enableUpdates = true
searchResultsNode.backgroundColor = .white
if let (layout, navigationBarHeight) = strongSelf.containerLayout {
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.top += strongSelf.tokenListNode.bounds.size.height
searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: .immediate)
searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size)
}
strongSelf.searchResultsReadyDisposable.set((searchResultsNode.ready |> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self, let searchResultsNode = strongSelf.searchResultsNode {
strongSelf.insertSubnode(searchResultsNode, aboveSubnode: strongSelf.contactListNode)
}
}))
}
}
}
}
}
deinit {
self.searchResultsReadyDisposable.dispose()
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
@ -40,16 +117,17 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
insets.top += navigationBarHeight
let tokenListHeight = self.tokenListNode.updateLayout(tokens: self.editableTokens, width: layout.size.width, transition: transition)
transition.updateFrame(node: self.tokenListNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: tokenListHeight)))
insets.top += tokenListHeight
self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition)
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
if let searchResultsNode = self.searchResultsNode {
searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition)
searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size)
}
}
}

View File

@ -0,0 +1,148 @@
import Foundation
import Display
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramCore
public class ContactSelectionController: ViewController {
private let account: Account
private var contactsNode: ContactSelectionControllerNode {
return self.displayNode as! ContactSelectionControllerNode
}
private let index: PeerNameIndex = .lastNameFirst
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private let _result = Promise<PeerId?>()
var result: Signal<PeerId?, NoError> {
return self._result.get()
}
private let createActionDisposable = MetaDisposable()
public init(account: Account, title: String) {
self.account = account
super.init()
self.title = title
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.contactsNode.contactListNode.scrollToTop()
}
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.createActionDisposable.dispose()
}
@objc func cancelPressed() {
self._result.set(.single(nil))
self.contactsNode.animateOut()
}
override public func loadDisplayNode() {
self.displayNode = ContactSelectionControllerNode(account: self.account)
self._ready.set(self.contactsNode.contactListNode.ready)
self.contactsNode.navigationBar = self.navigationBar
self.contactsNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch()
}
self.contactsNode.requestOpenPeerFromSearch = { [weak self] peerId in
self?.openPeer(peerId: peerId)
}
self.contactsNode.contactListNode.activateSearch = { [weak self] in
self?.activateSearch()
}
self.contactsNode.contactListNode.openPeer = { [weak self] peer in
self?.openPeer(peerId: peer.id)
}
self.contactsNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil)
}
self.displayNodeDidLoad()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments {
switch presentationArguments.presentationAnimation {
case .modalSheet:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelPressed))
case .none:
break
}
}
self.contactsNode.contactListNode.enableUpdates = true
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments {
switch presentationArguments.presentationAnimation {
case .modalSheet:
self.contactsNode.animateIn()
case .none:
break
}
}
}
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.contactsNode.contactListNode.enableUpdates = false
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
self.contactsNode.activateSearch()
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch() {
if !self.displayNavigationBar {
self.contactsNode.deactivateSearch()
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func openPeer(peerId: PeerId) {
self._result.set(.single(peerId))
self.contactsNode.animateOut()
}
}

View File

@ -0,0 +1,109 @@
import Display
import AsyncDisplayKit
import UIKit
import Postbox
import TelegramCore
final class ContactSelectionControllerNode: ASDisplayNode {
let contactListNode: ContactListNode
private let account: Account
private var searchDisplayController: SearchDisplayController?
private var containerLayout: (ContainerViewLayout, CGFloat)?
var navigationBar: NavigationBar?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((PeerId) -> Void)?
var dismiss: (() -> Void)?
init(account: Account) {
self.account = account
self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: []))
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor.white
self.addSubnode(self.contactListNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition)
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
func activateSearch() {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else {
return
}
var maybePlaceholderNode: SearchBarPlaceholderNode?
self.contactListNode.listNode.forEachItemNode { node in
if let node = node as? ChatListSearchItemNode {
maybePlaceholderNode = node.searchBarNode
}
}
if let _ = self.searchDisplayController {
return
}
if let placeholderNode = maybePlaceholderNode {
self.searchDisplayController = SearchDisplayController(contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in
if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch {
requestOpenPeerFromSearch(peerId)
}
}), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()
}
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { subnode in
self.insertSubnode(subnode, belowSubnode: navigationBar)
}, placeholder: placeholderNode)
}
}
func deactivateSearch() {
if let searchDisplayController = self.searchDisplayController {
var maybePlaceholderNode: SearchBarPlaceholderNode?
self.contactListNode.listNode.forEachItemNode { node in
if let node = node as? ChatListSearchItemNode {
maybePlaceholderNode = node.searchBarNode
}
}
searchDisplayController.deactivate(placeholder: maybePlaceholderNode)
self.searchDisplayController = nil
}
}
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?()
}
})
}
}

View File

@ -96,7 +96,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
func item(_ arguments: CreateGroupArguments) -> ListViewItem {
switch self {
case let .groupInfo(peer, state):
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in
arguments.updateEditingName(editingName)
})
case .setProfilePhoto:
@ -126,7 +126,7 @@ private func createGroupEntries(state: CreateGroupState, peerIds: [PeerId], view
let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil)
let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: "", photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0)
let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0)
entries.append(.groupInfo(peer, groupInfoState))
entries.append(.setProfilePhoto)

View File

@ -26,13 +26,13 @@ public class DataAndStorageSettingsController: ListController {
ListControllerDisclosureActionItem(title: "Bytes Received", action: deselectAction),
]
self.currentStatsDisposable = (((account.currentNetworkStats() |> then(Signal<MTNetworkUsageManagerStats, NoError>.complete() |> delay(1.0, queue: Queue.concurrentDefaultQueue()))) |> restart) |> deliverOnMainQueue).start(next: { [weak self] stats in
/*self.currentStatsDisposable = (((account.currentNetworkStats() |> then(Signal<MTNetworkUsageManagerStats, NoError>.complete() |> delay(1.0, queue: Queue.concurrentDefaultQueue()))) |> restart) |> deliverOnMainQueue).start(next: { [weak self] stats in
if let strongSelf = self {
let incoming = stats.wwan.incomingBytes + stats.other.incomingBytes
let outgoing = stats.wwan.outgoingBytes + stats.other.outgoingBytes
strongSelf.listDisplayNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: 0, previousIndex: 0, item: ListControllerDisclosureActionItem(title: "Bytes Sent: \(outgoing / 1024) KB", action: deselectAction), directionHint: nil), ListViewUpdateItem(index: 1, previousIndex: 1, item: ListControllerDisclosureActionItem(title: "Bytes Received: \(incoming / 1024) KB", action: deselectAction), directionHint: nil)], options: [.AnimateInsertion], updateOpaqueState: nil)
}
})
})*/
}
required public init(coder aDecoder: NSCoder) {

View File

@ -7,25 +7,50 @@ struct EditableTokenListToken {
let title: String
}
private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x3350ee))
private func caretAnimation() -> CAAnimation {
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber]
let firstDuration = 0.3
let secondDuration = 0.25
let restDuration = 0.35
let duration = firstDuration + secondDuration + restDuration
let keyTimes: [NSNumber] = [0.0 as NSNumber, (firstDuration / duration) as NSNumber, ((firstDuration + secondDuration) / duration) as NSNumber, ((firstDuration + secondDuration + restDuration) / duration) as NSNumber]
animation.keyTimes = keyTimes
animation.duration = duration
animation.repeatCount = Float.greatestFiniteMagnitude
return animation
}
private final class TokenNode: ASDisplayNode {
let token: EditableTokenListToken
let titleNode: ASTextNode
var isSelected: Bool {
didSet {
if self.isSelected != oldValue {
self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? UIColor(0x007ee5) : UIColor.black)
}
}
}
init(token: EditableTokenListToken) {
init(token: EditableTokenListToken, isSelected: Bool) {
self.token = token
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
self.titleNode.maximumNumberOfLines = 1
self.isSelected = isSelected
super.init()
self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: .black)
self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? UIColor(0x007ee5) : UIColor.black)
self.addSubnode(self.titleNode)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height))
return CGSize(width: titleSize.width + 8.0, height: 26.0)
return CGSize(width: titleSize.width + 8.0, height: 28.0)
}
override func layout() {
@ -37,10 +62,26 @@ private final class TokenNode: ASDisplayNode {
}
}
final class EditableTokenListNode: ASDisplayNode {
private final class CaretIndicatorNode: ASImageNode {
override func willEnterHierarchy() {
super.willEnterHierarchy()
if self.layer.animation(forKey: "blink") == nil {
self.layer.add(caretAnimation(), forKey: "blink")
}
}
}
final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
private let placeholderNode: ASTextNode
private var tokenNodes: [TokenNode] = []
private let separatorNode: ASDisplayNode
private let textFieldNode: TextFieldNode
private let caretIndicatorNode: CaretIndicatorNode
private var selectedTokenId: AnyHashable?
var textUpdated: ((String) -> Void)?
var deleteToken: ((AnyHashable) -> Void)?
override init() {
self.placeholderNode = ASTextNode()
@ -48,6 +89,15 @@ final class EditableTokenListNode: ASDisplayNode {
self.placeholderNode.maximumNumberOfLines = 1
self.placeholderNode.attributedText = NSAttributedString(string: "Whom would you like to message?", font: Font.regular(15.0), textColor: UIColor(0x8e8e92))
self.textFieldNode = TextFieldNode()
self.textFieldNode.textField.font = Font.regular(15.0)
self.caretIndicatorNode = CaretIndicatorNode()
self.caretIndicatorNode.isLayerBacked = true
self.caretIndicatorNode.displayWithoutProcessing = true
self.caretIndicatorNode.displaysAsynchronously = false
self.caretIndicatorNode.image = caretIndicatorImage
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.separatorNode.backgroundColor = UIColor(0xc7c6cb)
@ -57,7 +107,22 @@ final class EditableTokenListNode: ASDisplayNode {
self.backgroundColor = UIColor(0xf7f7f7)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.textFieldNode)
self.addSubnode(self.caretIndicatorNode)
self.clipsToBounds = true
self.textFieldNode.textField.delegate = self
self.textFieldNode.textField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
self.textFieldNode.textField.didDeleteBackwardWhileEmpty = { [weak self] in
if let strongSelf = self {
if let selectedTokenId = strongSelf.selectedTokenId {
strongSelf.deleteToken?(selectedTokenId)
} else if let tokenNode = strongSelf.tokenNodes.last {
strongSelf.selectedTokenId = tokenNode.token.id
tokenNode.isSelected = true
}
}
}
}
func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
@ -67,20 +132,30 @@ final class EditableTokenListNode: ASDisplayNode {
let tokenNode = tokenNodes[i]
if !validTokens.contains(tokenNode.token.id) {
self.tokenNodes.remove(at: i)
transition.updateAlpha(node: tokenNode, alpha: 0.0, completion: { [weak tokenNode] _ in
tokenNode?.removeFromSupernode()
})
if case .immediate = transition {
tokenNode.removeFromSupernode()
} else {
tokenNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tokenNode] _ in
tokenNode?.removeFromSupernode()
})
tokenNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
}
}
}
if let selectedTokenId = self.selectedTokenId, !validTokens.contains(selectedTokenId) {
self.selectedTokenId = nil
}
let sideInset: CGFloat = 4.0
let verticalInset: CGFloat = 7.0
let verticalInset: CGFloat = 6.0
let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
self.placeholderNode.frame = CGRect(origin: CGPoint(x: sideInset + 4.0, y: verticalInset + floor((26.0 - placeholderSize.height) / 2.0)), size: placeholderSize)
self.placeholderNode.frame = CGRect(origin: CGPoint(x: sideInset + 4.0, y: verticalInset + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize)
transition.updateAlpha(node: self.placeholderNode, alpha: tokens.isEmpty ? 1.0 : 0.0)
var animationDelay = 0.0
var currentOffset = CGPoint(x: sideInset, y: verticalInset)
for token in tokens {
var currentNode: TokenNode?
@ -95,7 +170,7 @@ final class EditableTokenListNode: ASDisplayNode {
if let currentNode = currentNode {
tokenNode = currentNode
} else {
tokenNode = TokenNode(token: token)
tokenNode = TokenNode(token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
self.tokenNodes.append(tokenNode)
self.addSubnode(tokenNode)
animateIn = true
@ -107,22 +182,92 @@ final class EditableTokenListNode: ASDisplayNode {
currentOffset.y += tokenSize.height
}
let tokenFrame = CGRect(origin: CGPoint(x: currentOffset.x, y: currentOffset.y), size: tokenSize)
currentOffset.x += tokenSize.width
currentOffset.x += ceil(tokenSize.width)
if animateIn {
tokenNode.frame = tokenFrame
tokenNode.alpha = 0.0
transition.updateAlpha(node: tokenNode, alpha: 1.0)
tokenNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
tokenNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
} else {
transition.updateFrame(node: tokenNode, frame: tokenFrame)
if case .immediate = transition {
transition.updateFrame(node: tokenNode, frame: tokenFrame)
} else {
let previousFrame = tokenNode.frame
if !previousFrame.origin.y.isEqual(to: tokenFrame.origin.y) && previousFrame.size.width.isEqual(to: tokenFrame.size.width) {
let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY)
let initialEndPosition = CGPoint(x: -previousFrame.size.width / 2.0, y: previousFrame.midY)
let targetStartPosition = CGPoint(x: width + tokenFrame.size.width, y: tokenFrame.midY)
let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY)
tokenNode.frame = tokenFrame
let initialAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: initialEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.12, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: targetStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
tokenNode.layer.animateGroup([initialAnimation, targetAnimation], key: "slide")
animationDelay += 0.025
} else {
if !previousFrame.size.width.isEqual(to: tokenFrame.size.width) {
tokenNode.frame = tokenFrame
} else {
let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY)
let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY)
tokenNode.frame = tokenFrame
let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil)
tokenNode.layer.animateGroup([targetAnimation], key: "slide")
animationDelay += 0.025
}
}
}
}
}
let nodeHeight = currentOffset.y + 28.0 + verticalInset
if width - currentOffset.x < 200.0 {
currentOffset.y += 28.0
currentOffset.x = sideInset
}
let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0))
let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel))
if case .immediate = transition {
transition.updateFrame(node: self.textFieldNode, frame: textNodeFrame)
transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame)
} else {
let previousFrame = self.textFieldNode.frame
self.textFieldNode.frame = textNodeFrame
self.textFieldNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
let previousCaretFrame = self.caretIndicatorNode.frame
self.caretIndicatorNode.frame = caretNodeFrame
self.caretIndicatorNode.layer.animateFrame(from: previousCaretFrame, to: caretNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
}
let nodeHeight = currentOffset.y + 29.0 + verticalInset
let separatorHeight = UIScreenPixel
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nodeHeight - separatorHeight), size: CGSize(width: width, height: separatorHeight)))
return nodeHeight
}
@objc func textFieldChanged(_ textField: UITextField) {
self.placeholderNode.isHidden = textField.text != nil && !textField.text!.isEmpty
self.textUpdated?(textField.text ?? "")
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if self.caretIndicatorNode.supernode == self {
self.caretIndicatorNode.removeFromSupernode()
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
if self.caretIndicatorNode.supernode != self {
self.addSubnode(self.caretIndicatorNode)
}
}
func setText(_ text: String) {
self.textFieldNode.textField.text = text
self.textFieldChanged(self.textFieldNode.textField)
}
}

View File

@ -0,0 +1,95 @@
import Foundation
extension UnicodeScalar {
var isEmoji: Bool {
switch value {
case 0x3030, 0x00AE, 0x00A9, // Special Characters
0x1D000 ... 0x1F77F, // Emoticons
0x2100 ... 0x27BF, // Misc symbols and Dingbats
0xFE00 ... 0xFE0F, // Variation Selectors
0x1F900 ... 0x1F9FF: // Supplemental Symbols and Pictographs
return true
default:
return false
}
}
var isZeroWidthJoiner: Bool {
return value == 8205
}
}
extension String {
var glyphCount: Int {
let richText = NSAttributedString(string: self)
let line = CTLineCreateWithAttributedString(richText)
return CTLineGetGlyphCount(line)
}
var isSingleEmoji: Bool {
return glyphCount == 1 && containsEmoji
}
var containsEmoji: Bool {
return !unicodeScalars.filter { $0.isEmoji }.isEmpty
}
var containsOnlyEmoji: Bool {
return unicodeScalars.first(where: { !$0.isEmoji && !$0.isZeroWidthJoiner }) == nil
}
// The next tricks are mostly to demonstrate how tricky it can be to determine emoji's
// If anyone has suggestions how to improve this, please let me know
var emojiString: String {
return emojiScalars.map { String($0) }.reduce("", +)
}
var emojis: [String] {
var scalars: [[UnicodeScalar]] = []
var currentScalarSet: [UnicodeScalar] = []
var previousScalar: UnicodeScalar?
for scalar in emojiScalars {
if let prev = previousScalar, !prev.isZeroWidthJoiner && !scalar.isZeroWidthJoiner {
scalars.append(currentScalarSet)
currentScalarSet = []
}
currentScalarSet.append(scalar)
previousScalar = scalar
}
scalars.append(currentScalarSet)
return scalars.map { $0.map{ String($0) } .reduce("", +) }
}
fileprivate var emojiScalars: [UnicodeScalar] {
var chars: [UnicodeScalar] = []
var previous: UnicodeScalar?
for cur in unicodeScalars {
if let previous = previous, previous.isZeroWidthJoiner && cur.isEmoji {
chars.append(previous)
chars.append(cur)
} else if cur.isEmoji {
chars.append(cur)
}
previous = cur
}
return chars
}
}

View File

@ -30,17 +30,39 @@ private func tagsForMessage(_ message: Message) -> MessageTags? {
return nil
}
private func galleryMediaForMedia(media: Media) -> Media? {
if let media = media as? TelegramMediaImage {
return media
} else if let file = media as? TelegramMediaFile {
if file.mimeType.hasPrefix("audio/") {
return nil
} else if !file.isVideo && file.mimeType.hasPrefix("video/") {
return file
} else {
return file
}
}
return nil
}
private func mediaForMessage(message: Message) -> Media? {
for media in message.media {
if let media = media as? TelegramMediaImage {
return media
} else if let file = media as? TelegramMediaFile {
if file.mimeType.hasPrefix("audio/") {
return nil
} else if !file.isVideo && file.mimeType.hasPrefix("video/") {
return file
} else {
return file
if let result = galleryMediaForMedia(media: media) {
return result
} else if let webpage = media as? TelegramMediaWebpage {
switch webpage.content {
case let .Loaded(content):
if let image = content.image {
if let result = galleryMediaForMedia(media: image) {
return result
}
} else if let file = content.file {
if let result = galleryMediaForMedia(media: file) {
return result
}
}
case .Pending:
break
}
}
}
@ -171,7 +193,7 @@ class GalleryController: ViewController {
if let strongSelf = self {
if let view = view {
strongSelf.entries = view.entries
loop: for i in 0 ..< strongSelf.entries.count {
loop: for i in 0 ..< strongSelf.entries.count {
switch strongSelf.entries[i] {
case let .MessageEntry(message, _, _) where message.id == messageId:
strongSelf.centralEntryIndex = i

View File

@ -8,6 +8,9 @@ final class GridHoleItem: GridItem {
func node(layout: GridNodeLayout) -> GridItemNode {
return GridHoleItemNode()
}
func update(node: GridItemNode) {
}
}
class GridHoleItemNode: GridItemNode {

View File

@ -41,6 +41,16 @@ final class GridMessageItem: GridItem {
}
return node
}
func update(node: GridItemNode) {
guard let node = node as? GridMessageItemNode else {
assertionFailure()
return
}
if let media = mediaForMessage(self.message) {
node.setup(account: self.account, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction)
}
}
}
final class GridMessageItemNode: GridItemNode {

View File

@ -92,10 +92,10 @@ enum GroupInfoEntry: PeerInfoEntry {
if !lhsCachedData.isEqual(to: rhsCachedData) {
return false
}
} else if (rhsCachedData == nil) != (rhsCachedData != nil) {
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
return false
}
if lhsState != lhsState {
if lhsState != rhsState {
return false
}
return true
@ -116,10 +116,10 @@ enum GroupInfoEntry: PeerInfoEntry {
}
case let .about(lhsText):
switch entry {
case let .about(lhsText):
return true
default:
return false
case .about(lhsText):
return true
default:
return false
}
case .sharedMedia:
switch entry {
@ -275,7 +275,7 @@ enum GroupInfoEntry: PeerInfoEntry {
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
switch self {
case let .info(peer, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in
})
case .setGroupPhoto:

View File

@ -143,7 +143,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
var insets = UIEdgeInsets()
let insets = UIEdgeInsets()
transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))

View File

@ -0,0 +1,111 @@
import Foundation
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
final class HorizontalStickerGridItem: GridItem {
let account: Account
let file: TelegramMediaFile
let interfaceInteraction: ChatPanelInterfaceInteraction
let section: GridSection? = nil
init(account: Account, file: TelegramMediaFile, interfaceInteraction: ChatPanelInterfaceInteraction) {
self.account = account
self.file = file
self.interfaceInteraction = interfaceInteraction
}
func node(layout: GridNodeLayout) -> GridItemNode {
let node = HorizontalStickerGridItemNode()
node.setup(account: self.account, file: self.file)
node.interfaceInteraction = self.interfaceInteraction
return node
}
func update(node: GridItemNode) {
guard let node = node as? HorizontalStickerGridItemNode else {
assertionFailure()
return
}
node.setup(account: self.account, file: self.file)
node.interfaceInteraction = self.interfaceInteraction
}
}
final class HorizontalStickerGridItemNode: GridItemNode {
private var currentState: (Account, TelegramMediaFile, CGSize)?
private let imageNode: TransformImageNode
private let stickerFetchedDisposable = MetaDisposable()
var interfaceInteraction: ChatPanelInterfaceInteraction?
override init() {
self.imageNode = TransformImageNode()
super.init()
self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
self.addSubnode(self.imageNode)
}
deinit {
stickerFetchedDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(account: Account, file: TelegramMediaFile) {
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.id != file.id {
if let dimensions = file.dimensions {
self.imageNode.setSignal(account: account, signal: chatMessageSticker(account: account, file: file, small: true))
self.stickerFetchedDisposable.set(fileInteractiveFetched(account: account, file: file).start())
self.currentState = (account, file, dimensions)
self.setNeedsLayout()
}
}
//self.updateSelectionState(animated: false)
//self.updateHiddenMedia()
}
override func layout() {
super.layout()
let bounds = self.bounds
let boundingSize = bounds.insetBy(dx: 2.0, dy: 2.0).size
if let (_, _, mediaDimensions) = self.currentState {
let imageSize = mediaDimensions.aspectFitted(boundingSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))()
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: CGSize(width: imageSize.width, height: imageSize.height))
self.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: imageSize.width, height: imageSize.height))
self.imageNode.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY)
}
}
/*func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? {
if self.messageId == id {
return self.imageNode
} else {
return nil
}
}*/
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state {
interfaceInteraction.sendSticker(item)
}
/*if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state {
controllerInteraction.openMessage(messageId)
}*/
}
}

View File

@ -0,0 +1,205 @@
import Foundation
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
private let backgroundCenterImage = generateImage(CGSize(width: 30.0, height: 82.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbfbfc4).cgColor)
context.setFillColor(UIColor.white.cgColor)
let lineWidth = UIScreenPixel
context.setLineWidth(lineWidth)
context.translateBy(x: 460.5, y: 364)
let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ")
context.fillPath()
context.translateBy(x: 0.0, y: lineWidth / 2.0)
let _ = try? drawSvgPath(context, path: "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 ")
context.strokePath()
context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0)
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
})
private let backgroundLeftImage = generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbfbfc4).cgColor)
context.setFillColor(UIColor.white.cgColor)
let lineWidth = UIScreenPixel
context.setLineWidth(lineWidth)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height)))
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height - lineWidth, height: size.height - lineWidth)))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
private struct StickerEntry: Identifiable, Comparable {
let index: Int
let file: TelegramMediaFile
var stableId: MediaId {
return self.file.fileId
}
static func ==(lhs: StickerEntry, rhs: StickerEntry) -> Bool {
return lhs.index == rhs.index && lhs.stableId == rhs.stableId
}
static func <(lhs: StickerEntry, rhs: StickerEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem {
let file = self.file
return HorizontalStickerGridItem(account: account, file: file, interfaceInteraction: interfaceInteraction)
}
}
private struct StickerEntryTransition {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let updateFirstIndexInSectionOffset: Int?
let stationaryItems: GridNodeStationaryItems
let scrollToItem: GridNodeScrollToItem?
}
private func preparedGridEntryTransition(account: Account, from fromEntries: [StickerEntry], to toEntries: [StickerEntry], interfaceInteraction: ChatPanelInterfaceInteraction) -> StickerEntryTransition {
let stationaryItems: GridNodeStationaryItems = .none
let scrollToItem: GridNodeScrollToItem? = nil
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) }
return StickerEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem)
}
final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode {
private let backgroundLeftNode: ASImageNode
private let backgroundNode: ASImageNode
private let backgroundRightNode: ASImageNode
private let clippingNode: ASDisplayNode
private let gridNode: GridNode
private var validLayout: (CGSize, ChatPresentationInterfaceState)?
private var currentEntries: [StickerEntry] = []
private var queuedTransitions: [StickerEntryTransition] = []
override init(account: Account) {
self.backgroundNode = ASImageNode()
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = backgroundCenterImage
self.backgroundLeftNode = ASImageNode()
self.backgroundLeftNode.displayWithoutProcessing = true
self.backgroundLeftNode.displaysAsynchronously = false
self.backgroundLeftNode.image = backgroundLeftImage
self.backgroundRightNode = ASImageNode()
self.backgroundRightNode.displayWithoutProcessing = true
self.backgroundRightNode.displaysAsynchronously = false
self.backgroundRightNode.image = backgroundLeftImage
self.backgroundRightNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.gridNode = GridNode()
self.gridNode.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
self.gridNode.view.disablesInteractiveTransitionGestureRecognizer = true
super.init(account: account)
self.isOpaque = false
self.addSubnode(self.backgroundNode)
self.addSubnode(self.backgroundLeftNode)
self.addSubnode(self.backgroundRightNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.gridNode)
}
func updateResults(_ results: [TelegramMediaFile]) {
let previousEntries = self.currentEntries
var entries: [StickerEntry] = []
for i in 0 ..< results.count {
entries.append(StickerEntry(index: i, file: results[i]))
}
self.currentEntries = entries
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, transition: .immediate, interfaceState: validLayout.1)
}
let transition = preparedGridEntryTransition(account: self.account, from: previousEntries, to: entries, interfaceInteraction: self.interfaceInteraction!)
self.enqueueTransition(transition)
}
private func enqueueTransition(_ transition: StickerEntryTransition) {
self.queuedTransitions.append(transition)
if self.validLayout != nil {
self.dequeueTransitions()
}
}
private func dequeueTransitions() {
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in })
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
let sideInsets: CGFloat = 10.0
let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries.count) * 66.0 + 6.0))
var leftInset: CGFloat = 40.0
var leftOffset: CGFloat = 0.0
if sideInsets + floor(contentWidth / 2.0) < sideInsets + leftInset + 15.0 {
let updatedLeftInset = sideInsets + floor(contentWidth / 2.0) - 15.0 - sideInsets
leftOffset = leftInset - updatedLeftInset
leftInset = updatedLeftInset
}
let backgroundFrame = CGRect(origin: CGPoint(x: sideInsets + leftOffset, y: size.height - 82.0 + 4.0), size: CGSize(width: contentWidth, height: 82.0))
let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: leftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: 82.0))
let backgroundRightFrame = CGRect(origin: CGPoint(x: backgroundCenterFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: max(0.0, backgroundFrame.minX + backgroundFrame.size.width - backgroundCenterFrame.maxX), height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
transition.updateFrame(node: self.backgroundLeftNode, frame: backgroundLeftFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundCenterFrame)
transition.updateFrame(node: self.backgroundRightNode, frame: backgroundRightFrame)
let gridFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 4.0), size: CGSize(width: backgroundFrame.size.width, height: 66.0))
self.clippingNode.frame = gridFrame
self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width))
let gridBounds = self.gridNode.bounds
self.gridNode.bounds = CGRect(x: gridBounds.minX, y: gridBounds.minY, width: gridFrame.size.height, height: gridFrame.size.width)
self.gridNode.position = CGPoint(x: gridFrame.size.width / 2.0, y: gridFrame.size.height / 2.0)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, itemSize: CGSize(width: 66.0, height: 66.0)), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in })
let dequeue = self.validLayout == nil
self.validLayout = (size, interfaceState)
if dequeue {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.dequeueTransitions()
}
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
completion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}

View File

@ -111,6 +111,7 @@ public class ImageNode: ASDisplayNode {
}
if !reportedHasImage {
if let hasImage = strongSelf.hasImage {
reportedHasImage = true
hasImage.set(true)
}
}

View File

@ -63,15 +63,17 @@ struct ItemListAvatarAndNameInfoItemState: Equatable {
class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem {
let account: Account
let peer: Peer?
let presence: PeerPresence?
let cachedData: CachedPeerData?
let state: ItemListAvatarAndNameInfoItemState
let sectionId: ItemListSectionId
let style: ItemListStyle
let editingNameUpdated: (ItemListAvatarAndNameInfoItemName) -> Void
init(account: Account, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void) {
init(account: Account, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void) {
self.account = account
self.peer = peer
self.presence = presence
self.cachedData = cachedData
self.state = state
self.sectionId = sectionId
@ -132,6 +134,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
private var inputSecondField: UITextField?
private var item: ItemListAvatarAndNameInfoItem?
private var layoutWidthAndNeighbors: (width: CGFloat, neighbors: ItemListNeighbors)?
private var peerPresenceManager: PeerPresenceStatusManager?
init() {
self.backgroundNode = ASDisplayNode()
@ -163,6 +167,13 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
self.addSubnode(self.avatarNode)
self.addSubnode(self.nameNode)
self.addSubnode(self.statusNode)
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let item = strongSelf.item, let layoutWidthAndNeighbors = strongSelf.layoutWidthAndNeighbors {
let (_, apply) = strongSelf.asyncLayout()(item, layoutWidthAndNeighbors.0, layoutWidthAndNeighbors.1)
apply(true)
}
})
}
func asyncLayout() -> (_ item: ItemListAvatarAndNameInfoItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
@ -183,9 +194,15 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
let statusText: String
let statusColor: UIColor
if let user = item.peer as? TelegramUser {
statusText = "online"
statusColor = UIColor(0x007ee5)
if let presence = item.presence as? TelegramUserPresence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp))
statusText = string
if activity {
statusColor = UIColor(0x007ee5)
} else {
statusColor = UIColor(0xb3b3b3)
}
} else if let channel = item.peer as? TelegramChannel {
if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
statusText = "\(memberCount) members"
@ -236,6 +253,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutWidthAndNeighbors = (width, neighbors)
let avatarOriginY: CGFloat
switch item.style {
@ -342,7 +360,6 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
if strongSelf.inputSecondField == nil {
let inputSecondField = TextFieldNodeView()
inputSecondField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
//inputSecondField.backgroundColor = UIColor.lightGray
inputSecondField.attributedPlaceholder = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce))
inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: UIColor.black)
strongSelf.inputSecondField = inputSecondField
@ -445,6 +462,9 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode {
strongSelf.nameNode.alpha = 1.0
}
}
if let presence = item.presence as? TelegramUserPresence {
strongSelf.peerPresenceManager?.reset(presence: presence)
}
}
})
}

View File

@ -3,13 +3,14 @@ import UIKit
import TelegramLegacyComponents
import Display
import SwiftSignalKit
import Postbox
func legacyAttachmentMenu(parentController: LegacyController, presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void) -> TGMenuSheetController {
func legacyAttachmentMenu(parentController: LegacyController, recentlyUsedInlineBots: [Peer], presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController {
let controller = TGMenuSheetController()
controller.applicationInterface = parentController.applicationInterface
controller.dismissesByOutsideTap = true
controller.hasSwipeGesture = true
//controller.maxHeight = 445.0 - TGMenuSheetButtonItemViewHeight
controller.maxHeight = 445.0 - TGMenuSheetButtonItemViewHeight
var itemViews: [Any] = []
@ -36,228 +37,53 @@ func legacyAttachmentMenu(parentController: LegacyController, presentOverlayCont
let galleryItem = TGMenuSheetButtonItemView(title: "Photo or Video", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in
controller?.dismiss(animated: true)
openGallery()
})
})!
itemViews.append(galleryItem)
let fileItem = TGMenuSheetButtonItemView(title: "File", type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
})
})!
itemViews.append(fileItem)
let locationItem = TGMenuSheetButtonItemView(title: "Location", type: TGMenuSheetButtonTypeDefault, action: {
})
let locationItem = TGMenuSheetButtonItemView(title: "Location", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in
controller?.dismiss(animated: true)
openMap()
})!
itemViews.append(locationItem)
let contactItem = TGMenuSheetButtonItemView(title: "Contact", type: TGMenuSheetButtonTypeDefault, action: {
})
let contactItem = TGMenuSheetButtonItemView(title: "Contact", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in
controller?.dismiss(animated: true)
openContacts()
})!
itemViews.append(contactItem)
carouselItem.underlyingViews = [galleryItem, fileItem]
for i in 0 ..< min(20, recentlyUsedInlineBots.count) {
let peer = recentlyUsedInlineBots[i]
let addressName = peer.addressName
if let addressName = addressName {
let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in
controller?.dismiss(animated: true)
selectRecentlyUsedInlineBot(peer)
})!
botItem.overflow = true
itemViews.append(botItem)
}
}
carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1)
let cancelItem = TGMenuSheetButtonItemView(title: "Cancel", type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in
controller?.dismiss(animated: true)
})
})!
itemViews.append(cancelItem)
controller.setItemViews(itemViews)
return controller
/*
carouselItem.condensed = !hasContactItem;
carouselItem.parentController = self;
carouselItem.allowCaptions = [_companion allowCaptionedMedia];
carouselItem.inhibitDocumentCaptions = [_companion encryptUploads];
__weak TGAttachmentCarouselItemView *weakCarouselItem = carouselItem;
carouselItem.suggestionContext = [self _suggestionContext];
carouselItem.cameraPressed = ^(TGAttachmentCameraView *cameraView)
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongSelf _displayCameraWithView:cameraView menuController:strongController];
};
carouselItem.sendPressed = ^(TGMediaAsset *currentItem, bool asFiles)
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
__strong TGAttachmentCarouselItemView *strongCarouselItem = weakCarouselItem;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
TGMediaAssetsControllerIntent intent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent;
[strongSelf _asyncProcessMediaAssetSignals:[TGMediaAssetsController resultSignalsForSelectionContext:strongCarouselItem.selectionContext editingContext:strongCarouselItem.editingContext intent:intent currentItem:currentItem storeAssets:[strongSelf->_companion controllerShouldStoreCapturedAssets] useMediaCache:[strongSelf->_companion controllerShouldCacheServerAssets] descriptionGenerator:^id(id result, NSString *caption, NSString *hash)
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf _descriptionForItem:result caption:caption hash:hash];
}]];
};
carouselItem.editorOpened = ^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf _updateCanReadHistory:TGModernConversationActivityChangeInactive];
};
carouselItem.editorClosed = ^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf _updateCanReadHistory:TGModernConversationActivityChangeActive];
};
[itemViews addObject:carouselItem];
TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.PhotoOrVideo") type:TGMenuSheetButtonTypeDefault action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
[strongSelf _displayMediaPicker:false fromFileMenu:false];
}];
galleryItem.longPressAction = ^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
[strongSelf _displayWebImagePicker];
};
[itemViews addObject:galleryItem];
TGMenuSheetButtonItemView *fileItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.File") type:TGMenuSheetButtonTypeDefault action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongSelf _displayFileMenuWithController:strongController];
}];
[itemViews addObject:fileItem];
carouselItem.underlyingViews = @[ galleryItem, fileItem ];
TGMenuSheetButtonItemView *locationItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Location") type:TGMenuSheetButtonTypeDefault action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
[strongSelf _displayLocationPicker];
}];
[itemViews addObject:locationItem];
if (hasContactItem)
{
TGMenuSheetButtonItemView *contactItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Contact") type:TGMenuSheetButtonTypeDefault action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
[strongSelf _displayContactPicker];
}];
[itemViews addObject:contactItem];
}
if (!TGIsPad()) {
NSArray<TGUser *> *inlineBots = [TGDatabaseInstance() _syncCachedRecentInlineBots];
NSUInteger counter = 0;
for (TGUser *user in inlineBots) {
if (user.userName.length == 0)
continue;
TGMenuSheetButtonItemView *botItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:[@"@" stringByAppendingString:user.userName] type:TGMenuSheetButtonTypeDefault action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
strongSelf->_inputTextPanel.inputField.userInteractionEnabled = true;
[strongSelf->_inputTextPanel.inputField setText:[NSString stringWithFormat:@"@%@ ", user.userName]];
[strongSelf openKeyboard];
}];
botItem.overflow = true;
[itemViews addObject:botItem];
counter++;
if (counter == 20) {
break;
}
}
}
carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * (itemViews.count - 1);
TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
}];
[itemViews addObject:cancelItem];
[controller setItemViews:itemViews];
[self.view endEditing:true];
[controller presentInViewController:self sourceView:_inputTextPanel.attachButton animated:true];*/
}
func legacyFileAttachmentMenu(menuSheetController: TGMenuSheetController) {

View File

@ -211,6 +211,7 @@ public class PeerMediaCollectionController: ViewController {
}, beginAudioRecording: {
}, finishAudioRecording: { _ in
}, setupMessageAutoremoveTimeout: {
}, sendSticker: { _ in
}, statuses: nil)
self.updateInterfaceState(animated: false, { return $0 })

View File

@ -61,7 +61,7 @@ public final class PeerSelectionController: ViewController {
}
}
self.peerSelectionNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in
/*self.peerSelectionNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in
if let strongSelf = self {
let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in
if modifier.getPeer(peer.id) == nil {
@ -76,7 +76,7 @@ public final class PeerSelectionController: ViewController {
}
}))
}
}
}*/
self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peer in
if let strongSelf = self {
@ -88,11 +88,10 @@ public final class PeerSelectionController: ViewController {
}
}
strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in
if let strongSelf = strongSelf {
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peer.id))
if let strongSelf = strongSelf, let peerSelected = strongSelf.peerSelected {
peerSelected(peer.id)
}
}))
}
}

View File

@ -0,0 +1,192 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
private func removeDuplicatedPlus(_ text: String?) -> String {
var result = ""
if let text = text {
for c in text.characters {
if c == "+" {
if result.isEmpty {
result += String(c)
}
} else {
result += String(c)
}
}
}
return result
}
private func removePlus(_ text: String?) -> String {
var result = ""
if let text = text {
for c in text.characters {
if c != "+" {
result += String(c)
}
}
}
return result
}
private func cleanPhoneNumber(_ text: String?) -> String {
var cleanNumber = ""
if let text = text {
for c in text.characters {
if c == "+" {
if cleanNumber.isEmpty {
cleanNumber += String(c)
}
} else if c >= "0" && c <= "9" {
cleanNumber += String(c)
}
}
}
return cleanNumber
}
private func cleanPrefix(_ text: String) -> String {
var result = ""
var checked = false
for c in text.characters {
if c != " " {
checked = true
}
if checked {
result += String(c)
}
}
return result
}
private func cleanSuffix(_ text: String) -> String {
var result = ""
var checked = false
for c in text.characters.reversed() {
if c != " " {
checked = true
}
if checked {
result = String(c) + result
}
}
return result
}
final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate {
let countryCodeField: TextFieldNode
let numberField: TextFieldNode
var previousCountryCodeText = "+"
var previousNumberText = ""
var enableEditing: Bool = true
var number: String {
get {
return cleanPhoneNumber((self.countryCodeField.textField.text ?? "") + (self.numberField.textField.text ?? ""))
} set(value) {
self.updateNumber(value)
}
}
var codeAndNumber: (Int32?, String) {
get {
var code: Int32?
if let text = self.countryCodeField.textField.text, let number = Int(removePlus(text)) {
code = Int32(number)
}
return (code, cleanPhoneNumber(self.numberField.textField.text))
} set(value) {
self.updateNumber("+" + (value.0 == nil ? "" : "\(value.0!)") + value.1)
}
}
var countryCodeUpdated: ((String) -> Void)?
private let phoneFormatter = InteractivePhoneFormatter()
override init() {
self.countryCodeField = TextFieldNode()
self.countryCodeField.textField.font = Font.regular(20.0)
self.countryCodeField.textField.textAlignment = .center
self.countryCodeField.textField.keyboardType = .numberPad
self.countryCodeField.textField.returnKeyType = .next
self.numberField = TextFieldNode()
self.numberField.textField.font = Font.regular(20.0)
self.numberField.textField.keyboardType = .numberPad
super.init()
self.addSubnode(self.countryCodeField)
self.addSubnode(self.numberField)
self.numberField.textField.didDeleteBackwardWhileEmpty = { [weak self] in
self?.countryCodeField.textField.becomeFirstResponder()
}
self.countryCodeField.textField.addTarget(self, action: #selector(self.countryCodeTextChanged(_:)), for: .editingChanged)
self.numberField.textField.addTarget(self, action: #selector(self.numberTextChanged(_:)), for: .editingChanged)
self.countryCodeField.textField.delegate = self
self.numberField.textField.delegate = self
}
@objc func countryCodeTextChanged(_ textField: UITextField) {
self.updateNumberFromTextFields()
}
@objc func numberTextChanged(_ textField: UITextField) {
self.updateNumberFromTextFields()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return self.enableEditing
}
private func updateNumberFromTextFields() {
let inputText = removeDuplicatedPlus(cleanPhoneNumber(self.countryCodeField.textField.text) + cleanPhoneNumber(self.numberField.textField.text))
self.updateNumber(inputText)
}
private func updateNumber(_ inputText: String) {
let (regionPrefix, text) = self.phoneFormatter.updateText(inputText)
var realRegionPrefix: String
let numberText: String
if let regionPrefix = regionPrefix, !regionPrefix.isEmpty {
realRegionPrefix = cleanSuffix(regionPrefix)
if !realRegionPrefix.hasPrefix("+") {
realRegionPrefix = "+" + realRegionPrefix
}
numberText = cleanPrefix(text.substring(from: realRegionPrefix.endIndex))
} else {
realRegionPrefix = text
if !realRegionPrefix.hasPrefix("+") {
realRegionPrefix = "+" + realRegionPrefix
}
numberText = ""
}
var focusOnNumber = false
if realRegionPrefix != self.countryCodeField.textField.text {
self.countryCodeField.textField.text = realRegionPrefix
}
if self.previousCountryCodeText != realRegionPrefix {
self.previousCountryCodeText = realRegionPrefix
self.countryCodeUpdated?(removePlus(realRegionPrefix).trimmingCharacters(in: CharacterSet.whitespaces))
}
if numberText != self.numberField.textField.text {
self.numberField.textField.text = numberText
}
if self.previousNumberText.isEmpty && !numberText.isEmpty {
focusOnNumber = true
}
self.previousNumberText = numberText
if focusOnNumber && !self.numberField.textField.isFirstResponder {
self.numberField.textField.becomeFirstResponder()
}
}
}

View File

@ -0,0 +1,66 @@
import Foundation
import AsyncDisplayKit
import Display
private let indicatorImage = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x007ee5).cgColor)
let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ")
/*
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>
<path d="M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z" id="Oval" stroke="none" fill="#159BEC" fill-rule="nonzero"></path>
</svg>
*/
})
final class ProgressNavigationButtonNode: ASDisplayNode {
private var indicatorNode: ASImageNode
override init() {
self.indicatorNode = ASImageNode()
self.indicatorNode.isLayerBacked = true
self.indicatorNode.displayWithoutProcessing = true
self.indicatorNode.displaysAsynchronously = false
self.indicatorNode.image = indicatorImage
super.init()
self.isLayerBacked = true
self.addSubnode(self.indicatorNode)
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = 0.5
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(M_PI * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation")
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 26.0, height: 22.0)
}
override func layout() {
super.layout()
let size = self.bounds.size
let indicatorSize = CGSize(width: 22.0, height: 22.0)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
}
}

View File

@ -4,18 +4,16 @@ import UIKit
import AsyncDisplayKit
import Display
private func generateBackground() -> UIImage? {
private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
let diameter: CGFloat = 8.0
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.setFillColor(UIColor.white.cgColor)
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0xededed).cgColor)
context.setFillColor(foregroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
}
private let searchBarBackground = generateBackground()
private class SearchBarPlaceholderNodeLayer: CALayer {
}
@ -29,6 +27,7 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate {
var activate: (() -> Void)?
let backgroundNode: ASImageNode
var foregroundColor: UIColor
let labelNode: TextNode
var placeholderString: NSAttributedString?
@ -38,12 +37,15 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate {
self.backgroundNode.isLayerBacked = false
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = searchBarBackground
self.foregroundColor = UIColor(0xededed)
self.backgroundNode.image = generateBackground(backgroundColor: UIColor.white, foregroundColor: self.foregroundColor)
self.labelNode = TextNode()
self.labelNode.isOpaque = true
self.labelNode.isLayerBacked = true
self.labelNode.backgroundColor = UIColor(0xededed)
self.labelNode.backgroundColor = self.foregroundColor
super.init()
/*super.init(viewBlock: {
@ -62,16 +64,28 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate {
self.backgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTap(_:))))
}
func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize) -> (() -> Void) {
func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ foregoundColor: UIColor) -> (() -> Void) {
let labelLayout = TextNode.asyncLayout(self.labelNode)
let currentForegroundColor = self.foregroundColor
return { placeholderString, constrainedSize in
let (labelLayoutResult, labelApply) = labelLayout(placeholderString, UIColor(0xededed), 1, .end, constrainedSize, nil)
return { placeholderString, constrainedSize, foregroundColor in
let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, nil)
var updatedBackgroundImage: UIImage?
if !currentForegroundColor.isEqual(foregroundColor) {
updatedBackgroundImage = generateBackground(backgroundColor: UIColor.white, foregroundColor: foregroundColor)
}
return { [weak self] in
if let strongSelf = self {
let _ = labelApply()
strongSelf.foregroundColor = foregroundColor
if let updatedBackgroundImage = updatedBackgroundImage {
strongSelf.backgroundNode.image = updatedBackgroundImage
strongSelf.labelNode.backgroundColor = foregroundColor
}
strongSelf.placeholderString = placeholderString
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - labelLayoutResult.size.width) / 2.0), y: floor((28.0 - labelLayoutResult.size.height) / 2.0) + 2.0), size: labelLayoutResult.size)

View File

@ -8,6 +8,7 @@ private struct SettingsItemArguments {
let account: Account
let pushController: (ViewController) -> Void
let presentController: (ViewController) -> Void
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let saveEditingState: () -> Void
}
@ -171,15 +172,22 @@ private enum SettingsEntry: ItemListNodeEntry {
func item(_ arguments: SettingsItemArguments) -> ListViewItem {
switch self {
case let .userInfo(peer, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in
arguments.updateEditingName(editingName)
})
case .setProfilePhoto:
return ItemListActionItem(title: "Set Profile Photo", kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.presentController(standardTextAlertController(title: "Verification Failed", text: "Your Apple ID or password is incorrect.", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .defaultAction, title: "OK", action: {
})
]))
})
case .notificationsAndSounds:
return ItemListDisclosureItem(title: "Notifications ans Sounds", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
return ItemListDisclosureItem(title: "Notifications and Sounds", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.pushController(notificationsAndSoundsController(account: arguments.account))
})
case .privacyAndSecurity:
@ -290,6 +298,7 @@ public func settingsController(account: Account) -> ViewController {
}
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
@ -298,6 +307,8 @@ public func settingsController(account: Account) -> ViewController {
let arguments = SettingsItemArguments(account: account, pushController: { controller in
pushControllerImpl?(controller)
}, presentController: { controller in
presentControllerImpl?(controller)
}, updateEditingName: { editingName in
updateState { state in
if let _ = state.editingState {
@ -362,5 +373,8 @@ public func settingsController(account: Account) -> ViewController {
pushControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.pushViewController(value)
}
presentControllerImpl = { [weak controller] value in
controller?.present(value, in: .window)
}
return controller
}

View File

@ -0,0 +1,35 @@
import Foundation
import UIKit
extension CALayer {
func addShakeAnimation() {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let duration = 0.3
let amplitude: CGFloat = 3.0
let animation = CAKeyframeAnimation(keyPath: "position.x")
let values: [CGFloat] = [0.0, amplitude, -amplitude, amplitude, -amplitude, 0.0]
animation.values = values.map { ($0 as NSNumber) as AnyObject }
var keyTimes: [NSNumber] = []
for i in 0 ..< values.count {
if i == 0 {
keyTimes.append(0.0)
} else if i == values.count - 1 {
keyTimes.append(1.0)
} else {
keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber)
}
}
animation.keyTimes = keyTimes
animation.speed = speed
animation.duration = duration
animation.isAdditive = true
self.add(animation, forKey: "shake")
}
}

View File

@ -5,6 +5,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba
var nsString: NSString?
let string = NSMutableAttributedString(string: text, attributes: [NSFontAttributeName: baseFont, NSForegroundColorAttributeName: UIColor.black])
var skipEntity = false
let stringLength = string.length
for i in 0 ..< entities.count {
if skipEntity {
skipEntity = false
@ -15,10 +16,9 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba
if nsString == nil {
nsString = text as NSString
}
if range.location + range.length > nsString!.length {
let upperBound = nsString!.length
range.location = max(0, upperBound - range.length)
range.length = upperBound - range.location
if range.location + range.length > stringLength {
range.location = max(0, stringLength - range.length)
range.length = stringLength - range.location
}
switch entity.type {
case .Url:

View File

@ -12,4 +12,5 @@ module TelegramUIPrivateModule {
header "../UIImage+WebP.h"
header "../RingBuffer.h"
header "../TelegramUIIncludes.h"
header "../../third-party/RMIntro/platform/ios/RMIntroViewController.h"
}

View File

@ -314,9 +314,20 @@ final class TextNode: ASDisplayNode {
var updated = false
if let existingLayout = existingLayout, existingLayout.constrainedSize == constrainedSize && existingLayout.maximumNumberOfLines == maximumNumberOfLines && existingLayout.truncationType == truncationType && existingLayout.cutout == cutout {
let stringMatch: Bool
if let existingString = existingLayout.attributedString, let string = attributedString {
var colorMatch: Bool = true
if let backgroundColor = backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor {
if !backgroundColor.isEqual(previousBackgroundColor) {
colorMatch = false
}
} else if (backgroundColor != nil) != (existingLayout.backgroundColor != nil) {
colorMatch = false
}
if !colorMatch {
stringMatch = false
} else if let existingString = existingLayout.attributedString, let string = attributedString {
stringMatch = existingString.isEqual(to: string)
} else if existingLayout.attributedString == nil && attributedString == nil {
stringMatch = true

View File

@ -17,7 +17,7 @@ enum DestructiveUserInfoAction {
}
enum UserInfoEntry: PeerInfoEntry {
case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
case about(text: String)
case phoneNumber(index: Int, value: PhoneNumberWithLabel)
case userName(value: String)
@ -53,14 +53,21 @@ enum UserInfoEntry: PeerInfoEntry {
}
switch self {
case let .info(lhsPeer, lhsCachedData, lhsState):
case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState):
switch entry {
case let .info(rhsPeer, rhsCachedData, rhsState):
case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState):
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer == nil) != (rhsPeer != nil) {
} else if (lhsPeer != nil) != (rhsPeer != nil) {
return false
}
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if !lhsPresence.isEqual(to: rhsPresence) {
return false
}
} else if (lhsPresence != nil) != (rhsPresence != nil) {
return false
}
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
@ -205,8 +212,8 @@ enum UserInfoEntry: PeerInfoEntry {
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
switch self {
case let .info(peer, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
case let .info(peer, presence, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
})
case let .about(text):
@ -314,7 +321,7 @@ func userInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries {
}
}
entries.append(UserInfoEntry.info(peer: user, cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: updatingName)))
entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: updatingName)))
if let cachedUserData = view.cachedData as? CachedUserData {
if let about = cachedUserData.about, !about.isEmpty {
entries.append(UserInfoEntry.about(text: about))

4
third-party/RMIntro/3rdparty/.gitignore vendored Executable file
View File

@ -0,0 +1,4 @@
libglm/
libpng/*
!libpng/Android.mk
zlib/

529
third-party/RMIntro/3rdparty/linmath/linmath.h vendored Executable file
View File

@ -0,0 +1,529 @@
#ifndef LINMATH_H
#define LINMATH_H
#include <math.h>
typedef float vec2[2];
typedef float vec3[3];
static inline void vec3_add(vec3 r, vec3 a, vec3 b)
{
int i;
for(i=0; i<3; ++i)
r[i] = a[i] + b[i];
}
static inline void vec3_sub(vec3 r, vec3 a, vec3 b)
{
int i;
for(i=0; i<3; ++i)
r[i] = a[i] - b[i];
}
static inline void vec3_scale(vec3 r, vec3 v, float s)
{
int i;
for(i=0; i<3; ++i)
r[i] = v[i] * s;
}
static inline float vec3_mul_inner(vec3 a, vec3 b)
{
float p = 0.f;
int i;
for(i=0; i<3; ++i)
p += b[i]*a[i];
return p;
}
static inline void vec3_mul_cross(vec3 r, vec3 a, vec3 b)
{
r[0] = a[1]*b[2] - a[2]*b[1];
r[1] = a[2]*b[0] - a[0]*b[2];
r[2] = a[0]*b[1] - a[1]*b[0];
}
static inline float vec3_len(vec3 v)
{
return sqrtf(vec3_mul_inner(v, v));
}
static inline void vec3_norm(vec3 r, vec3 v)
{
float k = 1.f / vec3_len(v);
vec3_scale(r, v, k);
}
/////
static inline void vec2_add(vec2 r, vec2 a, vec2 b)
{
int i;
for(i=0; i<2; ++i)
r[i] = a[i] + b[i];
}
static inline float vec2_mul_inner(vec2 a, vec2 b)
{
float p = 0.f;
int i;
for(i=0; i<2; ++i)
p += b[i]*a[i];
return p;
}
static inline float vec2_len(vec2 v)
{
return sqrtf(vec2_mul_inner(v, v));
}
static inline void vec2_scale(vec2 r, vec2 v, float s)
{
int i;
for(i=0; i<2; ++i)
r[i] = v[i] * s;
}
static inline void vec2_norm(vec2 r, vec2 v)
{
float k = 1.f / vec2_len(v);
vec2_scale(r, v, k);
}
////
static inline void vec3_reflect(vec3 r, vec3 v, vec3 n)
{
float p = 2.f*vec3_mul_inner(v, n);
int i;
for(i=0;i<3;++i)
r[i] = v[i] - p*n[i];
}
typedef float vec4[4];
static inline void vec4_add(vec4 r, vec4 a, vec4 b)
{
int i;
for(i=0; i<4; ++i)
r[i] = a[i] + b[i];
}
static inline void vec4_sub(vec4 r, vec4 a, vec4 b)
{
int i;
for(i=0; i<4; ++i)
r[i] = a[i] - b[i];
}
static inline void vec4_scale(vec4 r, vec4 v, float s)
{
int i;
for(i=0; i<4; ++i)
r[i] = v[i] * s;
}
static inline float vec4_mul_inner(vec4 a, vec4 b)
{
float p = 0.f;
int i;
for(i=0; i<4; ++i)
p += b[i]*a[i];
return p;
}
static inline void vec4_mul_cross(vec4 r, vec4 a, vec4 b)
{
r[0] = a[1]*b[2] - a[2]*b[1];
r[1] = a[2]*b[0] - a[0]*b[2];
r[2] = a[0]*b[1] - a[1]*b[0];
r[3] = 1.f;
}
static inline float vec4_len(vec4 v)
{
return sqrtf(vec4_mul_inner(v, v));
}
static inline void vec4_norm(vec4 r, vec4 v)
{
float k = 1.f / vec4_len(v);
vec4_scale(r, v, k);
}
static inline void vec4_reflect(vec4 r, vec4 v, vec4 n)
{
float p = 2.f*vec4_mul_inner(v, n);
int i;
for(i=0;i<4;++i)
r[i] = v[i] - p*n[i];
}
typedef vec4 mat4x4[4];
static inline void mat4x4_identity(mat4x4 M)
{
int i, j;
for(i=0; i<4; ++i)
for(j=0; j<4; ++j)
M[i][j] = i==j ? 1.f : 0.f;
}
static inline void mat4x4_dup(mat4x4 M, mat4x4 N)
{
int i, j;
for(i=0; i<4; ++i)
for(j=0; j<4; ++j)
M[i][j] = N[i][j];
}
static inline void mat4x4_row(vec4 r, mat4x4 M, int i)
{
int k;
for(k=0; k<4; ++k)
r[k] = M[k][i];
}
static inline void mat4x4_col(vec4 r, mat4x4 M, int i)
{
int k;
for(k=0; k<4; ++k)
r[k] = M[i][k];
}
static inline void mat4x4_transpose(mat4x4 M, mat4x4 N)
{
int i, j;
for(j=0; j<4; ++j)
for(i=0; i<4; ++i)
M[i][j] = N[j][i];
}
static inline void mat4x4_add(mat4x4 M, mat4x4 a, mat4x4 b)
{
int i;
for(i=0; i<4; ++i)
vec4_add(M[i], a[i], b[i]);
}
static inline void mat4x4_sub(mat4x4 M, mat4x4 a, mat4x4 b)
{
int i;
for(i=0; i<4; ++i)
vec4_sub(M[i], a[i], b[i]);
}
static inline void mat4x4_scale(mat4x4 M, mat4x4 a, float k)
{
int i;
for(i=0; i<4; ++i)
vec4_scale(M[i], a[i], k);
}
static inline void mat4x4_scale_XYZ(mat4x4 M, float x, float y, float z)
{
mat4x4 a;
mat4x4_dup(a, M);
vec4_scale(M[0], a[0], x);
vec4_scale(M[1], a[1], y);
vec4_scale(M[2], a[2], z);
}
static inline void mat4x4_scale_aniso(mat4x4 M, mat4x4 a, float x, float y, float z)
{
vec4_scale(M[0], a[0], x);
vec4_scale(M[1], a[1], y);
vec4_scale(M[2], a[2], z);
}
static inline void mat4x4_mul(mat4x4 M, mat4x4 a, mat4x4 b)
{
int k, r, c;
for(c=0; c<4; ++c) for(r=0; r<4; ++r) {
M[c][r] = 0.f;
for(k=0; k<4; ++k)
M[c][r] += a[k][r] * b[c][k];
}
}
static inline void mat4x4_mul_vec4(vec4 r, mat4x4 M, vec4 v)
{
int i, j;
for(j=0; j<4; ++j) {
r[j] = 0.f;
for(i=0; i<4; ++i)
r[j] += M[i][j] * v[i];
}
}
static inline void mat4x4_translate(mat4x4 T, float x, float y, float z)
{
mat4x4_identity(T);
T[3][0] = x;
T[3][1] = y;
T[3][2] = z;
}
static inline void mat4x4_from_vec3_mul_outer(mat4x4 M, vec3 a, vec3 b)
{
int i, j;
for(i=0; i<4; ++i) for(j=0; j<4; ++j)
M[i][j] = i<3 && j<3 ? a[i] * b[j] : 0.f;
}
static inline void mat4x4_rotate(mat4x4 R, mat4x4 M, float x, float y, float z, float angle)
{
float s = sinf(angle);
float c = cosf(angle);
vec3 u = {x, y, z};
vec3_norm(u, u);
{
mat4x4 T;
mat4x4_from_vec3_mul_outer(T, u, u);
mat4x4 S = {
{ 0.f, u[2], -u[1], 0.f},
{-u[2], 0.f, u[0], 0.f},
{ u[1], -u[0], 0.f, 0.f},
{ 0.f, 0.f, 0.f, 0.f}
};
mat4x4_scale(S, S, s);
mat4x4 C;
mat4x4_identity(C);
mat4x4_sub(C, C, T);
mat4x4_scale(C, C, c);
mat4x4_add(T, T, C);
mat4x4_add(T, T, S);
T[3][3] = 1.f;
mat4x4_mul(R, M, T);
}
}
static inline void mat4x4_rotate_X(mat4x4 Q, mat4x4 M, float angle)
{
float s = sinf(angle);
float c = cosf(angle);
mat4x4 R = {
{1.f, 0.f, 0.f, 0.f},
{0.f, c, s, 0.f},
{0.f, -s, c, 0.f},
{0.f, 0.f, 0.f, 1.f}
};
mat4x4_mul(Q, M, R);
}
static inline void mat4x4_rotate_Y(mat4x4 Q, mat4x4 M, float angle)
{
float s = sinf(angle);
float c = cosf(angle);
mat4x4 R = {
{ c, 0.f, s, 0.f},
{ 0.f, 1.f, 0.f, 0.f},
{ -s, 0.f, c, 0.f},
{ 0.f, 0.f, 0.f, 1.f}
};
mat4x4_mul(Q, M, R);
}
static inline void mat4x4_rotate_Z2(mat4x4 Q, mat4x4 M, float angle)
{
float s = sinf(angle);
float c = cosf(angle);
mat4x4 R = {
{ c, s, 0.f, 0.f},
{ -s, c, 0.f, 0.f},
{ 0.f, 0.f, 1.f, 0.f},
{ 0.f, 0.f, 0.f, 1.f}
};
mat4x4_mul(Q, M, R);
}
static inline void mat4x4_rotate_Z(mat4x4 Q, float angle)
{
mat4x4 temp;
mat4x4_dup(temp, Q);
mat4x4_rotate_Z2(Q, temp, angle);
}
static inline void mat4x4_invert(mat4x4 T, mat4x4 M)
{
float s[6];
float c[6];
s[0] = M[0][0]*M[1][1] - M[1][0]*M[0][1];
s[1] = M[0][0]*M[1][2] - M[1][0]*M[0][2];
s[2] = M[0][0]*M[1][3] - M[1][0]*M[0][3];
s[3] = M[0][1]*M[1][2] - M[1][1]*M[0][2];
s[4] = M[0][1]*M[1][3] - M[1][1]*M[0][3];
s[5] = M[0][2]*M[1][3] - M[1][2]*M[0][3];
c[0] = M[2][0]*M[3][1] - M[3][0]*M[2][1];
c[1] = M[2][0]*M[3][2] - M[3][0]*M[2][2];
c[2] = M[2][0]*M[3][3] - M[3][0]*M[2][3];
c[3] = M[2][1]*M[3][2] - M[3][1]*M[2][2];
c[4] = M[2][1]*M[3][3] - M[3][1]*M[2][3];
c[5] = M[2][2]*M[3][3] - M[3][2]*M[2][3];
/* Assumes it is invertible */
float idet = 1.0f/( s[0]*c[5]-s[1]*c[4]+s[2]*c[3]+s[3]*c[2]-s[4]*c[1]+s[5]*c[0] );
T[0][0] = ( M[1][1] * c[5] - M[1][2] * c[4] + M[1][3] * c[3]) * idet;
T[0][1] = (-M[0][1] * c[5] + M[0][2] * c[4] - M[0][3] * c[3]) * idet;
T[0][2] = ( M[3][1] * s[5] - M[3][2] * s[4] + M[3][3] * s[3]) * idet;
T[0][3] = (-M[2][1] * s[5] + M[2][2] * s[4] - M[2][3] * s[3]) * idet;
T[1][0] = (-M[1][0] * c[5] + M[1][2] * c[2] - M[1][3] * c[1]) * idet;
T[1][1] = ( M[0][0] * c[5] - M[0][2] * c[2] + M[0][3] * c[1]) * idet;
T[1][2] = (-M[3][0] * s[5] + M[3][2] * s[2] - M[3][3] * s[1]) * idet;
T[1][3] = ( M[2][0] * s[5] - M[2][2] * s[2] + M[2][3] * s[1]) * idet;
T[2][0] = ( M[1][0] * c[4] - M[1][1] * c[2] + M[1][3] * c[0]) * idet;
T[2][1] = (-M[0][0] * c[4] + M[0][1] * c[2] - M[0][3] * c[0]) * idet;
T[2][2] = ( M[3][0] * s[4] - M[3][1] * s[2] + M[3][3] * s[0]) * idet;
T[2][3] = (-M[2][0] * s[4] + M[2][1] * s[2] - M[2][3] * s[0]) * idet;
T[3][0] = (-M[1][0] * c[3] + M[1][1] * c[1] - M[1][2] * c[0]) * idet;
T[3][1] = ( M[0][0] * c[3] - M[0][1] * c[1] + M[0][2] * c[0]) * idet;
T[3][2] = (-M[3][0] * s[3] + M[3][1] * s[1] - M[3][2] * s[0]) * idet;
T[3][3] = ( M[2][0] * s[3] - M[2][1] * s[1] + M[2][2] * s[0]) * idet;
}
static inline void mat4x4_frustum(mat4x4 M, float l, float r, float b, float t, float n, float f)
{
M[0][0] = 2.f*n/(r-l);
M[0][1] = M[0][2] = M[0][3] = 0.f;
M[1][1] = 2.0f*n/(t-b);
M[1][0] = M[1][2] = M[1][3] = 0.f;
M[2][0] = (r+l)/(r-l);
M[2][1] = (t+b)/(t-b);
M[2][2] = -(f+n)/(f-n);
M[2][3] = -1.f;
M[3][2] = -2.f*(f*n)/(f-n);
M[3][0] = M[3][1] = M[3][3] = 0.f;
}
static inline void mat4x4_ortho(mat4x4 M, float l, float r, float b, float t, float n, float f)
{
M[0][0] = 2.f/(r-l);
M[0][1] = M[0][2] = M[0][3] = 0.f;
M[1][1] = 2.f/(t-b);
M[1][0] = M[1][2] = M[1][3] = 0.f;
M[2][2] = -2.f/(f-n);
M[2][0] = M[2][1] = M[2][3] = 0.f;
M[3][0] = -(r+l)/(r-l);
M[3][1] = -(t+b)/(t-b);
M[3][2] = -(f+n)/(f-n);
M[3][3] = 1.f;
}
typedef float quat[4];
static inline void quat_identity(quat q)
{
q[0] = q[1] = q[2] = 0.f;
q[3] = 1.f;
}
static inline void quat_add(quat r, quat a, quat b)
{
int i;
for(i=0; i<4; ++i)
r[i] = a[i] + b[i];
}
static inline void quat_sub(quat r, quat a, quat b)
{
int i;
for(i=0; i<4; ++i)
r[i] = a[i] - b[i];
}
static inline void quat_mul(quat r, quat p, quat q)
{
vec3 w;
vec3_mul_cross(r, p, q);
vec3_scale(w, p, q[3]);
vec3_add(r, r, w);
vec3_scale(w, q, p[3]);
vec3_add(r, r, w);
r[3] = p[3]*q[3] - vec3_mul_inner(p, q);
}
static inline void quat_scale(quat r, quat v, float s)
{
int i;
for(i=0; i<4; ++i)
r[i] = v[i] * s;
}
static inline float quat_inner_product(quat a, quat b)
{
float p = 0.f;
int i;
for(i=0; i<4; ++i)
p += b[i]*a[i];
return p;
}
static inline void quat_conj(quat r, quat q)
{
int i;
for(i=0; i<3; ++i)
r[i] = -q[i];
r[3] = q[3];
}
#define quat_norm vec4_norm
static inline void quat_mul_vec3(vec3 r, quat q, vec3 v)
{
quat v_ = {v[0], v[1], v[2], 0.f};
quat_conj(r, q);
quat_norm(r, r);
quat_mul(r, v_, r);
quat_mul(r, q, r);
}
static inline void mat4x4_from_quat(mat4x4 M, quat q)
{
float a = q[3];
float b = q[0];
float c = q[1];
float d = q[2];
float a2 = a*a;
float b2 = b*b;
float c2 = c*c;
float d2 = d*d;
M[0][0] = a2 + b2 - c2 - d2;
M[0][1] = 2.f*(b*c + a*d);
M[0][2] = 2.f*(b*d - a*c);
M[0][3] = 0.f;
M[1][0] = 2*(b*c - a*d);
M[1][1] = a2 - b2 + c2 - d2;
M[1][2] = 2.f*(c*d + a*b);
M[1][3] = 0.f;
M[2][0] = 2.f*(b*d + a*c);
M[2][1] = 2.f*(c*d - a*b);
M[2][2] = a2 - b2 - c2 + d2;
M[2][3] = 0.f;
M[3][0] = M[3][1] = M[3][2] = 0.f;
M[3][3] = 1.f;
}
static inline void mat4x4_mul_quat(mat4x4 R, mat4x4 M, quat q)
{
quat_mul_vec3(R[0], M[0], q);
quat_mul_vec3(R[1], M[1], q);
quat_mul_vec3(R[2], M[2], q);
R[3][0] = R[3][1] = R[3][2] = 0.f;
R[3][3] = 1.f;
}
static inline void quat_from_mat4x4(quat q, mat4x4 M)
{
float r=0.f;
int i;
int perm[] = { 0, 1, 2, 0, 1 };
int *p = perm;
for(i = 0; i<3; i++) {
float m = M[i][i];
if( m < r )
continue;
m = r;
p = &perm[i];
}
r = sqrtf(1.f + M[p[0]][p[0]] - M[p[1]][p[1]] - M[p[2]][p[2]] );
if(r < 1e-6) {
q[0] = 1.f;
q[1] = q[2] = q[3] = 0.f;
return;
}
q[0] = r/2.f;
q[1] = (M[p[0]][p[1]] - M[p[1]][p[0]])/(2.f*r);
q[2] = (M[p[2]][p[0]] - M[p[0]][p[2]])/(2.f*r);
q[3] = (M[p[2]][p[1]] - M[p[1]][p[2]])/(2.f*r);
}
#endif

2672
third-party/RMIntro/core/animations.c vendored Normal file

File diff suppressed because it is too large Load Diff

45
third-party/RMIntro/core/animations.h vendored Executable file
View File

@ -0,0 +1,45 @@
//
// animations.h
// IntroOpenGL
//
// Created by Ilya Rimchikov on 29/03/14.
// Copyright (c) 2014 IntroOpenGL. All rights reserved.
//
void on_surface_created();
void on_surface_changed(int a_width_px, int a_height_px, float a_scale_factor, int a1, int a2, int a3, int a4, int a5);
void on_draw_frame();
void set_touch_x(int a);
void set_date(double a);
void set_date0(double a);
void set_page(int page);
void set_pages_textures(int a1, int a2, int a3, int a4, int a5, int a6);
void set_ic_textures(int a_ic_bubble_dot, int a_ic_bubble, int a_ic_cam_lens, int a_ic_cam, int a_ic_pencil, int a_ic_pin, int a_ic_smile_eye, int a_ic_smile, int a_ic_videocam);
void set_telegram_textures(int a_telegram_sphere, int a_telegram_plane);
void set_fast_textures(int a_fast_body, int a_fast_spiral, int a_fast_arrow, int a_fast_arrow_shadow);
void set_free_textures(int a_knot_up, int a_knot_down);
void set_powerful_textures(int a_powerful_mask, int a_powerful_star, int a_powerful_infinity, int a_powerful_infinity_white);
void set_private_textures(int a_private_door, int a_private_screw);
void set_ui_textures(int a_pagination_dot);
void set_need_pages(int a);
void set_y_offset(float a);
void set_scroll_offset(float a_offset);
void inc_stars_rendered();
void set_elements_top_margins(int a_icon_y, int a_text_y, int a_button_y);

19
third-party/RMIntro/core/buffer.c vendored Executable file
View File

@ -0,0 +1,19 @@
#include "buffer.h"
#include "platform_gl.h"
#include <assert.h>
#include <stdlib.h>
GLuint create_vbo(const GLsizeiptr size, const GLvoid* data, const GLenum usage) {
assert(data != NULL);
GLuint vbo_object;
glGenBuffers(1, &vbo_object);
assert(vbo_object != 0);
glBindBuffer(GL_ARRAY_BUFFER, vbo_object);
//glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage)
glBufferData(GL_ARRAY_BUFFER, size, data, usage);
glBindBuffer(GL_ARRAY_BUFFER, 0);
return vbo_object;
}

6
third-party/RMIntro/core/buffer.h vendored Executable file
View File

@ -0,0 +1,6 @@
#include "platform_gl.h"
#define BUFFER_OFFSET(i) ((void*)(i))
GLuint create_vbo(const GLsizeiptr size, const GLvoid* data, const GLenum usage);
GLuint create_vbo2(const GLsizeiptr vertex_data_size, const GLvoid* vertex_data, const GLsizeiptr color_data_size, const GLvoid* color_data, const GLenum usage);

1
third-party/RMIntro/core/config.h vendored Executable file
View File

@ -0,0 +1 @@
#define LOGGING_ON 0

1
third-party/RMIntro/core/macros.h vendored Executable file
View File

@ -0,0 +1 @@
#define UNUSED(x) (void)(x)

13
third-party/RMIntro/core/math_helper.h vendored Executable file
View File

@ -0,0 +1,13 @@
#include <math.h>
static inline float deg_to_radf(float deg) {
return deg * (float)M_PI / 180.0f;
}
static inline double MAXf(float a, float b) {
return a > b ? a : b;
}
static inline double MINf(float a, float b) {
return a < b ? a : b;
}

100
third-party/RMIntro/core/matrix.h vendored Executable file
View File

@ -0,0 +1,100 @@
#include "linmath.h"
#include <math.h>
#include <string.h>
/* Adapted from Android's OpenGL Matrix.java. */
static inline void mat4x4_perspective(mat4x4 m, float y_fov_in_degrees, float aspect, float n, float f)
{
const float angle_in_radians = (float) (y_fov_in_degrees * M_PI / 180.0);
const float a = (float) (1.0 / tan(angle_in_radians / 2.0));
m[0][0] = a / aspect;
m[1][0] = 0.0f;
m[2][0] = 0.0f;
m[3][0] = 0.0f;
m[1][0] = 0.0f;
m[1][1] = a;
m[1][2] = 0.0f;
m[1][3] = 0.0f;
m[2][0] = 0.0f;
m[2][1] = 0.0f;
m[2][2] = -((f + n) / (f - n));
m[2][3] = -1.0f;
m[3][0] = 0.0f;
m[3][1] = 0.0f;
m[3][2] = -((2.0f * f * n) / (f - n));
m[3][3] = 0.0f;
}
static inline void mat4x4_translate_in_place(mat4x4 m, float x, float y, float z)
{
int i;
for (i = 0; i < 4; ++i) {
m[3][i] += m[0][i] * x
+ m[1][i] * y
+ m[2][i] * z;
}
}
static inline void mat4x4_look_at(mat4x4 m,
float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ)
{
// See the OpenGL GLUT documentation for gluLookAt for a description
// of the algorithm. We implement it in a straightforward way:
float fx = centerX - eyeX;
float fy = centerY - eyeY;
float fz = centerZ - eyeZ;
// Normalize f
vec3 f_vec = {fx, fy, fz};
float rlf = 1.0f / vec3_len(f_vec);
fx *= rlf;
fy *= rlf;
fz *= rlf;
// compute s = f x up (x means "cross product")
float sx = fy * upZ - fz * upY;
float sy = fz * upX - fx * upZ;
float sz = fx * upY - fy * upX;
// and normalize s
vec3 s_vec = {sx, sy, sz};
float rls = 1.0f / vec3_len(s_vec);
sx *= rls;
sy *= rls;
sz *= rls;
// compute u = s x f
float ux = sy * fz - sz * fy;
float uy = sz * fx - sx * fz;
float uz = sx * fy - sy * fx;
m[0][0] = sx;
m[0][1] = ux;
m[0][2] = -fx;
m[0][3] = 0.0f;
m[1][0] = sy;
m[1][1] = uy;
m[1][2] = -fy;
m[1][3] = 0.0f;
m[2][0] = sz;
m[2][1] = uz;
m[2][2] = -fz;
m[2][3] = 0.0f;
m[3][0] = 0.0f;
m[3][1] = 0.0f;
m[3][2] = 0.0f;
m[3][3] = 1.0f;
mat4x4_translate_in_place(m, -eyeX, -eyeY, -eyeZ);
}

1302
third-party/RMIntro/core/objects.c vendored Normal file

File diff suppressed because it is too large Load Diff

176
third-party/RMIntro/core/objects.h vendored Executable file
View File

@ -0,0 +1,176 @@
//
// objects.h
// IntroOpenGL
//
// Created by Ilya Rimchikov on 29/03/14.
// Copyright (c) 2014 IntroOpenGL. All rights reserved.
//
#include "platform_gl.h"
#include "program.h"
#include "linmath.h"
extern float scale_factor;
extern int width, height;
extern int y_offset_absolute;
typedef enum {NORMAL, NORMAL_ONE, RED, BLUE, LIGHT_RED, LIGHT_BLUE} texture_program_type;
typedef struct {
float x;
float y;
} CPoint;
typedef struct {
float width;
float height;
} CSize;
typedef struct {
float r;
float g;
float b;
float a;
} CColor;
CPoint CPointMake(float x, float y);
CSize CSizeMake(float width, float height);
float D2R(float a);
float R2D(float a);
typedef struct {
float x;
float y;
float z;
} xyz;
xyz xyzMake(float x, float y, float z);
typedef struct {
float side_length;
float start_angle;
float end_angle;
float angle;
CSize size;
float radius;
float width;
} VarParams;
typedef struct {
int datasize;
int round_count;
GLenum triangle_mode;
int is_star;
} ConstParams;
typedef struct {
xyz anchor;
xyz position;
float rotation;
xyz scale;
} LayerParams;
typedef struct {
xyz anchor;
xyz position;
float rotation;
xyz scale;
float alpha;
VarParams var_params;
ConstParams const_params;
LayerParams *layer_params;
} Params;
typedef struct {
vec4 color;
CPoint *data;
GLuint buffer;
int num_points;
Params params;
} Shape;
typedef struct {
GLuint texture;
CPoint *data;
GLuint buffer;
int num_points;
Params params;
} TexturedShape;
Params default_params();
LayerParams default_layer_params();
void mat4x4_translate_independed(mat4x4 m, float x, float y, float z);
void set_y_offset_objects(float a);
void setup_shaders();
void draw_shape(const Shape* shape, mat4x4 view_projection_matrix);
void draw_textured_shape(const TexturedShape* shape, mat4x4 view_projection_matrix, texture_program_type program_type);
TexturedShape create_segmented_square(float side_length, float start_angle, float end_angle, GLuint texture);
void change_segmented_square(TexturedShape* shape, float side_length, float start_angle, float end_angle);
Shape create_rounded_rectangle(CSize size, float radius, int round_count, const vec4 color);
void change_rounded_rectangle(Shape* shape, CSize size, float radius);
Shape create_rectangle(CSize size, const vec4 color);
Shape create_circle(float radius, int vertex_count, const vec4 color);
void change_circle(Shape* shape, float radius);
void draw_colored_rectangle(const Shape* shape, mat4x4 view_projection_matrix);
TexturedShape create_textured_rectangle(CSize size, GLuint texture);
void change_textured_rectangle(TexturedShape* shape, CSize size);
Shape create_ribbon(float length, const vec4 color);
void change_ribbon(Shape* shape, float length);
Shape create_segmented_circle(float radius, int vertex_count, float start_angle, float angle, const vec4 color);
void change_segmented_circle(Shape* shape, float radius, float start_angle, float angle);
Shape create_infinity(float width, float angle, int segment_count, const vec4 color);
void change_infinity(Shape* shape, float angle);
void draw_infinity(const Shape* shape, mat4x4 view_projection_matrix);
Shape create_rounded_rectangle_stroked(CSize size, float radius, float stroke_width, int round_count, const vec4 color);
void change_rounded_rectangle_stroked(Shape* shape, CSize size, float radius, float stroke_width);
Shape create_rounded_rectangle(CSize size, float radius, int round_count, const vec4 color);
void change_rounded_rectangle(Shape* shape, CSize size, float radius);
void mat4x4_log(mat4x4 M);

41
third-party/RMIntro/core/program.c vendored Executable file
View File

@ -0,0 +1,41 @@
#include "program.h"
#include "platform_gl.h"
TextureProgram get_texture_program(GLuint program)
{
return (TextureProgram) {
program,
glGetAttribLocation(program, "a_Position"),
glGetAttribLocation(program, "a_TextureCoordinates"),
glGetUniformLocation(program, "u_MvpMatrix"),
glGetUniformLocation(program, "u_TextureUnit"),
glGetUniformLocation(program, "u_Alpha")};
}
ColorProgram get_color_program(GLuint program)
{
return (ColorProgram) {
program,
glGetAttribLocation(program, "a_Position"),
glGetUniformLocation(program, "u_MvpMatrix"),
glGetUniformLocation(program, "u_Color"),
glGetUniformLocation(program, "u_Alpha")};
}
GradientProgram get_gradient_program(GLuint program)
{
return (GradientProgram) {
program,
glGetAttribLocation(program, "a_Position"),
glGetUniformLocation(program, "u_MvpMatrix"),
glGetAttribLocation(program, "a_Color"),
glGetUniformLocation(program, "u_Alpha")};
}

37
third-party/RMIntro/core/program.h vendored Executable file
View File

@ -0,0 +1,37 @@
#pragma once
#include "platform_gl.h"
typedef struct {
GLuint program;
GLint a_position_location;
GLint a_texture_coordinates_location;
GLint u_mvp_matrix_location;
GLint u_texture_unit_location;
GLint u_alpha_loaction;
} TextureProgram;
typedef struct {
GLuint program;
GLint a_position_location;
GLint u_mvp_matrix_location;
GLint u_color_location;
GLint u_alpha_loaction;
} ColorProgram;
typedef struct {
GLuint program;
GLint a_position_location;
GLint u_mvp_matrix_location;
GLint a_color_location;
GLint u_alpha_loaction;
} GradientProgram;
TextureProgram get_texture_program(GLuint program);
ColorProgram get_color_program(GLuint program);
GradientProgram get_gradient_program(GLuint program);

28
third-party/RMIntro/core/rngs.c vendored Normal file
View File

@ -0,0 +1,28 @@
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include "rngs.h"
float frand(float from, float to)
{
// srand(time(NULL));
return (float)(((double)random()/RAND_MAX)*(to-from)+from);
}
int irand(int from, int to)
{
// srand(time(NULL));
return (int)(((double)random()/RAND_MAX)*(to-from+1)+from);
}
int signrand()
{
// srand(time(NULL));
return irand(0, 1)*2-1;
}

7
third-party/RMIntro/core/rngs.h vendored Normal file
View File

@ -0,0 +1,7 @@
int irand(int from, int to);
float frand(float from, float to);
int signrand();

110
third-party/RMIntro/core/shader.c vendored Executable file
View File

@ -0,0 +1,110 @@
#include "shader.h"
#include "platform_gl.h"
#include "platform_log.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#define TAG "shaders"
static void log_v_fixed_length(const GLchar* source, const GLint length) {
if (LOGGING_ON) {
char log_buffer[length + 1];
memcpy(log_buffer, source, length);
log_buffer[length] = '\0';
DEBUG_LOG_WRITE_V(TAG, log_buffer);
}
}
static void log_shader_info_log(GLuint shader_object_id) {
if (LOGGING_ON) {
GLint log_length;
glGetShaderiv(shader_object_id, GL_INFO_LOG_LENGTH, &log_length);
GLchar log_buffer[log_length];
glGetShaderInfoLog(shader_object_id, log_length, NULL, log_buffer);
DEBUG_LOG_WRITE_V(TAG, log_buffer);
}
}
static void log_program_info_log(GLuint program_object_id) {
if (LOGGING_ON) {
GLint log_length;
glGetProgramiv(program_object_id, GL_INFO_LOG_LENGTH, &log_length);
GLchar log_buffer[log_length];
glGetProgramInfoLog(program_object_id, log_length, NULL, log_buffer);
DEBUG_LOG_WRITE_V(TAG, log_buffer);
}
}
GLuint compile_shader(const GLenum type, const GLchar* source, const GLint length) {
assert(source != NULL);
GLuint shader_object_id = glCreateShader(type);
GLint compile_status;
assert(shader_object_id != 0);
glShaderSource(shader_object_id, 1, (const GLchar **)&source, &length);
glCompileShader(shader_object_id);
glGetShaderiv(shader_object_id, GL_COMPILE_STATUS, &compile_status);
if (LOGGING_ON) {
DEBUG_LOG_WRITE_D(TAG, "Results of compiling shader source:");
log_v_fixed_length(source, length);
log_shader_info_log(shader_object_id);
}
assert(compile_status != 0);
return shader_object_id;
}
GLuint link_program(const GLuint vertex_shader, const GLuint fragment_shader) {
GLuint program_object_id = glCreateProgram();
GLint link_status;
assert(program_object_id != 0);
glAttachShader(program_object_id, vertex_shader);
glAttachShader(program_object_id, fragment_shader);
glLinkProgram(program_object_id);
glGetProgramiv(program_object_id, GL_LINK_STATUS, &link_status);
if (LOGGING_ON) {
DEBUG_LOG_WRITE_D(TAG, "Results of linking program:");
log_program_info_log(program_object_id);
}
assert(link_status != 0);
return program_object_id;
}
GLuint build_program(
const GLchar * vertex_shader_source, const GLint vertex_shader_source_length,
const GLchar * fragment_shader_source, const GLint fragment_shader_source_length) {
assert(vertex_shader_source != NULL);
assert(fragment_shader_source != NULL);
GLuint vertex_shader = compile_shader(
GL_VERTEX_SHADER, vertex_shader_source, vertex_shader_source_length);
GLuint fragment_shader = compile_shader(
GL_FRAGMENT_SHADER, fragment_shader_source, fragment_shader_source_length);
return link_program(vertex_shader, fragment_shader);
}
GLint validate_program(const GLuint program) {
if (LOGGING_ON) {
int validate_status;
glValidateProgram(program);
glGetProgramiv(program, GL_VALIDATE_STATUS, &validate_status);
DEBUG_LOG_PRINT_D(TAG, "Results of validating program: %d", validate_status);
log_program_info_log(program);
return validate_status;
}
return 0;
}

10
third-party/RMIntro/core/shader.h vendored Executable file
View File

@ -0,0 +1,10 @@
#include "platform_gl.h"
GLuint compile_shader(const GLenum type, const GLchar* source, const GLint length);
GLuint link_program(const GLuint vertex_shader, const GLuint fragment_shader);
GLuint build_program(
const GLchar * vertex_shader_source, const GLint vertex_shader_source_length,
const GLchar * fragment_shader_source, const GLint fragment_shader_source_length);
/* Should be called just before using a program to draw, if validation is needed. */
GLint validate_program(const GLuint program);

139
third-party/RMIntro/core/timing.c vendored Normal file
View File

@ -0,0 +1,139 @@
//
// timing.c
// IntroOpenGL
//
// Created by Ilya Rimchikov on 03/05/14.
// Copyright (c) 2014 IntroOpenGL. All rights reserved.
//
#include "timing.h"
static inline float evaluateAtParameterWithCoefficients(float t, float coefficients[])
{
return coefficients[0] + t*coefficients[1] + t*t*coefficients[2] + t*t*t*coefficients[3];
}
static inline float evaluateDerivationAtParameterWithCoefficients(float t, float coefficients[])
{
return coefficients[1] + 2*t*coefficients[2] + 3*t*t*coefficients[3];
}
static inline float calcParameterViaNewtonRaphsonUsingXAndCoefficientsForX(float x, float coefficientsX[])
{
// see http://en.wikipedia.org/wiki/Newton's_method
// start with X being the correct value
float t = x;
// iterate several times
//const float epsilon = 0.00001;
int i;
for(i = 0; i < 10; i++)
{
float x2 = evaluateAtParameterWithCoefficients(t, coefficientsX) - x;
float d = evaluateDerivationAtParameterWithCoefficients(t, coefficientsX);
float dt = x2/d;
t = t - dt;
}
return t;
}
static inline float calcParameterUsingXAndCoefficientsForX (float x, float coefficientsX[])
{
// for the time being, we'll guess Newton-Raphson always
// returns the correct value.
// if we find it doesn't find the solution often enough,
// we can add additional calculation methods.
float t = calcParameterViaNewtonRaphsonUsingXAndCoefficientsForX(x, coefficientsX);
return t;
}
static int is_initialized=0;
static float _coefficientsX[TIMING_NUM][4], _coefficientsY[TIMING_NUM][4];
static const float _c0x = 0.0;
static const float _c0y = 0.0;
static const float _c3x = 1.0;
static const float _c3y = 1.0;
float timing(float x, timing_type type)
{
if (is_initialized==0) {
is_initialized=1;
float c[TIMING_NUM][4];
c[Default][0]=0.25f;
c[Default][1]=0.1f;
c[Default][2]=0.25f;
c[Default][3]=1.0f;
c[EaseInEaseOut][0]=0.42f;
c[EaseInEaseOut][1]=0.0f;
c[EaseInEaseOut][2]=0.58f;
c[EaseInEaseOut][3]=1.0f;
c[EaseIn][0]=0.42f;
c[EaseIn][1]=0.0f;
c[EaseIn][2]=1.0f;
c[EaseIn][3]=1.0f;
c[EaseOut][0]=0.0f;
c[EaseOut][1]=0.0f;
c[EaseOut][2]=0.58f;
c[EaseOut][3]=1.0f;
c[EaseOutBounce][0]=0.0;
c[EaseOutBounce][1]=0.0;
c[EaseOutBounce][2]=0.;
c[EaseOutBounce][3]=1.25;
c[Linear][0]=0.0;
c[Linear][1]=0.0;
c[Linear][2]=1.0;
c[Linear][3]=1.0;
int i;
for (i=0; i<TIMING_NUM; i++) {
float _c1x = c[i][0];
float _c1y = c[i][1];
float _c2x = c[i][2];
float _c2y = c[i][3];
_coefficientsX[i][0] = _c0x; // t^0
_coefficientsX[i][1] = -3.0f*_c0x + 3.0f*_c1x; // t^1
_coefficientsX[i][2] = 3.0f*_c0x - 6.0f*_c1x + 3.0f*_c2x; // t^2
_coefficientsX[i][3] = -_c0x + 3.0f*_c1x - 3.0f*_c2x + _c3x; // t^3
_coefficientsY[i][0] = _c0y; // t^0
_coefficientsY[i][1] = -3.0f*_c0y + 3.0f*_c1y; // t^1
_coefficientsY[i][2] = 3.0f*_c0y - 6.0f*_c1y + 3.0f*_c2y; // t^2
_coefficientsY[i][3] = -_c0y + 3.0f*_c1y - 3.0f*_c2y + _c3y; // t^3
}
}
if (x==0.0 || x==1.0) {
return x;
}
float t = calcParameterUsingXAndCoefficientsForX(x, _coefficientsX[type]);
float y = evaluateAtParameterWithCoefficients(t, _coefficientsY[type]);
return y;
}

22
third-party/RMIntro/core/timing.h vendored Normal file
View File

@ -0,0 +1,22 @@
//
// timing.h
// IntroOpenGL
//
// Created by Ilya Rimchikov on 03/05/14.
// Copyright (c) 2014 IntroOpenGL. All rights reserved.
//
typedef enum
{
Default=0,
EaseIn=1,
EaseOut=2,
EaseInEaseOut=3,
Linear=4,
Sin=5,
EaseOutBounce,
TIMING_NUM
} timing_type;
float timing(float x, timing_type type);

View File

@ -0,0 +1,27 @@
#include "platform_log.h"
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#define LOG_VPRINTF(priority) printf("(" priority ") %s: ", tag); \
va_list arg_ptr; \
va_start(arg_ptr, fmt); \
vprintf(fmt, arg_ptr); \
va_end(arg_ptr); \
printf("\n");
void _debug_log_v(const char *tag, const char *fmt, ...) {
LOG_VPRINTF("VERBOSE");
}
void _debug_log_d(const char *tag, const char *fmt, ...) {
LOG_VPRINTF("DEBUG");
}
void _debug_log_w(const char *tag, const char *fmt, ...) {
LOG_VPRINTF("WARN");
}
void _debug_log_e(const char *tag, const char *fmt, ...) {
LOG_VPRINTF("ERROR");
}

View File

@ -0,0 +1,19 @@
#include "platform_macros.h"
#include "../../core/config.h"
void _debug_log_v(const char* tag, const char* text, ...) PRINTF_ATTRIBUTE(2, 3);
void _debug_log_d(const char* tag, const char* text, ...) PRINTF_ATTRIBUTE(2, 3);
void _debug_log_w(const char* tag, const char* text, ...) PRINTF_ATTRIBUTE(2, 3);
void _debug_log_e(const char* tag, const char* text, ...) PRINTF_ATTRIBUTE(2, 3);
#define DEBUG_LOG_PRINT_V(tag, fmt, ...) do { if (LOGGING_ON) _debug_log_v(tag, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
#define DEBUG_LOG_PRINT_D(tag, fmt, ...) do { if (LOGGING_ON) _debug_log_d(tag, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
#define DEBUG_LOG_PRINT_W(tag, fmt, ...) do { if (LOGGING_ON) _debug_log_w(tag, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
#define DEBUG_LOG_PRINT_E(tag, fmt, ...) do { if (LOGGING_ON) _debug_log_e(tag, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
#define DEBUG_LOG_WRITE_V(tag, text) DEBUG_LOG_PRINT_V(tag, "%s", text)
#define DEBUG_LOG_WRITE_D(tag, text) DEBUG_LOG_PRINT_D(tag, "%s", text)
#define DEBUG_LOG_WRITE_W(tag, text) DEBUG_LOG_PRINT_W(tag, "%s", text)
#define DEBUG_LOG_WRITE_E(tag, text) DEBUG_LOG_PRINT_E(tag, "%s", text)
#define CRASH(e) DEBUG_LOG_WRITE_E("Assert", #e); __builtin_trap()

View File

@ -0,0 +1,5 @@
#if defined(__GNUC__)
#define PRINTF_ATTRIBUTE(format_pos, arg_pos) __attribute__((format(printf, format_pos, arg_pos)))
#else
#define PRINTF_ATTRIBUTE(format_pos, arg_pos)
#endif

34
third-party/RMIntro/platform/ios/.gitignore vendored Executable file
View File

@ -0,0 +1,34 @@
# Based from http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects
# Temporary files
.DS_Store
.Trashes
*.swp
*.lock
*~.nib
# Meta files
*._*
# Build files
DerivedData/
build/
# Private settings
# *.pbxuser should not be ignored if using "custom executables".
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
# Some projects need the below
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
# XCode 4 semi-personal settings
xcuserdata/
# Deprecated classes
*.moved-aside

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6254" systemVersion="14A329f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Intro 2.0" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
third-party/RMIntro/platform/ios/Default.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Some files were not shown because too many files have changed in this diff Show More