no message

This commit is contained in:
Peter
2017-02-19 23:21:03 +03:00
parent 32efb5962d
commit a0ccb729be
51 changed files with 3898 additions and 1101 deletions

View File

@@ -38,7 +38,7 @@
D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; }; D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; };
D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */; }; D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */; };
D01B27991E39144C0022A4C0 /* ItemListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27981E39144C0022A4C0 /* ItemListController.swift */; }; D01B27991E39144C0022A4C0 /* ItemListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27981E39144C0022A4C0 /* ItemListController.swift */; };
D01B279B1E39386C0022A4C0 /* SettingsControllerEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279A1E39386C0022A4C0 /* SettingsControllerEntries.swift */; }; D01B279B1E39386C0022A4C0 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279A1E39386C0022A4C0 /* SettingsController.swift */; };
D01B279D1E394A500022A4C0 /* NotificationsAndSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */; }; D01B279D1E394A500022A4C0 /* NotificationsAndSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */; };
D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */; }; D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */; };
D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */; }; D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */; };
@@ -92,6 +92,9 @@
D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; }; D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; };
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; }; D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; };
D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; }; D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; };
D03E5E091E55C49C0029569A /* DebugAccountsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E5E081E55C49C0029569A /* DebugAccountsController.swift */; };
D03E5E0F1E55F8B90029569A /* ChannelVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */; };
D0486F0A1E523C8500091F0C /* GroupInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0486F091E523C8500091F0C /* GroupInfoController.swift */; };
D049EAE21E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */; }; D049EAE21E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */; };
D049EAE41E44949F00A2CD3A /* HorizontalStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */; }; D049EAE41E44949F00A2CD3A /* HorizontalStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */; };
D049EAE61E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */; }; D049EAE61E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */; };
@@ -170,6 +173,10 @@
D050F2131E48B61500988324 /* PhoneInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2121E48B61500988324 /* PhoneInputNode.swift */; }; D050F2131E48B61500988324 /* PhoneInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2121E48B61500988324 /* PhoneInputNode.swift */; };
D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */; }; D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */; };
D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */; }; D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */; };
D0561DDF1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */; };
D0561DE11E57153000E6B9E9 /* ItemListActivityTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */; };
D0561DE61E57424700E6B9E9 /* ItemListMultilineTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */; };
D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */; };
D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; }; D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; };
D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; }; D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; };
D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; };
@@ -243,6 +250,7 @@
D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */; }; D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */; };
D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; }; D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; };
D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */; }; D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */; };
D0B98E7F1E575D2C008084B1 /* ChannelBlacklistController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */; };
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; }; D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; };
D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; }; D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; };
D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; }; D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; };
@@ -253,6 +261,7 @@
D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */; }; D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */; };
D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */; }; D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */; };
D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */; }; D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */; };
D0CE1BD31E51BC6100404327 /* DebugController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE1BD21E51BC6100404327 /* DebugController.swift */; };
D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */; }; D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */; };
D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; }; D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; };
D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; }; D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; };
@@ -358,13 +367,6 @@
D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */; }; D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */; };
D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */; }; D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */; };
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */; }; D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */; };
D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */; };
D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */; };
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */; };
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */; };
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */; };
D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */; };
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */; };
D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */; }; D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */; };
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; }; D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; };
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; }; D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; };
@@ -424,7 +426,6 @@
D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */; }; D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */; };
D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */; }; D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */; };
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */; }; D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */; };
D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */; };
D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */; }; D0F69E881D6B8C850046BCD6 /* FastBlur.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */; };
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E801D6B8C850046BCD6 /* FastBlur.m */; }; D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E801D6B8C850046BCD6 /* FastBlur.m */; };
D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */; }; D0F69E8A1D6B8C850046BCD6 /* FFMpegSwResample.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */; };
@@ -497,7 +498,7 @@
D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditAccessoryPanelNode.swift; sourceTree = "<group>"; }; D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditAccessoryPanelNode.swift; sourceTree = "<group>"; };
D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerNode.swift; sourceTree = "<group>"; }; D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerNode.swift; sourceTree = "<group>"; };
D01B27981E39144C0022A4C0 /* ItemListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListController.swift; sourceTree = "<group>"; }; D01B27981E39144C0022A4C0 /* ItemListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListController.swift; sourceTree = "<group>"; };
D01B279A1E39386C0022A4C0 /* SettingsControllerEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsControllerEntries.swift; sourceTree = "<group>"; }; D01B279A1E39386C0022A4C0 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsAndSounds.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettings.swift; sourceTree = "<group>"; };
@@ -551,6 +552,9 @@
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
D03E5E081E55C49C0029569A /* DebugAccountsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugAccountsController.swift; sourceTree = "<group>"; };
D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelVisibilityController.swift; sourceTree = "<group>"; };
D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = "<group>"; };
D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.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>"; }; 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>"; }; D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputRecentStickerPacksItem.swift; sourceTree = "<group>"; };
@@ -629,6 +633,10 @@
D050F2121E48B61500988324 /* PhoneInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneInputNode.swift; 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>"; }; 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>"; }; D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionControllerNode.swift; sourceTree = "<group>"; };
D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSingleLineInputItem.swift; sourceTree = "<group>"; };
D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActivityTextItem.swift; sourceTree = "<group>"; };
D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListMultilineTextItem.swift; sourceTree = "<group>"; };
D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminsController.swift; sourceTree = "<group>"; };
D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.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>"; }; 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>"; }; D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = "<group>"; };
@@ -705,6 +713,7 @@
D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = "<group>"; }; D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = "<group>"; };
D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = "<group>"; }; D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = "<group>"; };
D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceStatusManager.swift; sourceTree = "<group>"; }; D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceStatusManager.swift; sourceTree = "<group>"; };
D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlacklistController.swift; sourceTree = "<group>"; };
D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = "<group>"; }; D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = "<group>"; };
D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = "<group>"; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = "<group>"; };
D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = "<group>"; }; D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = "<group>"; };
@@ -715,6 +724,7 @@
D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatButtonKeyboardInputNode.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>"; }; 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>"; }; D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = "<group>"; };
D0CE1BD21E51BC6100404327 /* DebugController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugController.swift; sourceTree = "<group>"; };
D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = "<group>"; }; D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = "<group>"; };
D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = "<group>"; }; D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = "<group>"; };
D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = "<group>"; }; D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = "<group>"; };
@@ -820,13 +830,6 @@
D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerItem.swift; sourceTree = "<group>"; }; D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerItem.swift; sourceTree = "<group>"; };
D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerNode.swift; sourceTree = "<group>"; }; D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerNode.swift; sourceTree = "<group>"; };
D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerSpacerItem.swift; sourceTree = "<group>"; }; D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerSpacerItem.swift; sourceTree = "<group>"; };
D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeController.swift; sourceTree = "<group>"; };
D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeControllerNode.swift; sourceTree = "<group>"; };
D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationController.swift; sourceTree = "<group>"; };
D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordController.swift; sourceTree = "<group>"; };
D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = "<group>"; };
D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = "<group>"; };
D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = "<group>"; };
D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarNode.swift; sourceTree = "<group>"; }; D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarNode.swift; sourceTree = "<group>"; };
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; }; D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; };
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = "<group>"; }; D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = "<group>"; };
@@ -886,7 +889,6 @@
D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSectionHeaderAccessoryItem.swift; sourceTree = "<group>"; }; D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSectionHeaderAccessoryItem.swift; sourceTree = "<group>"; };
D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsVCardItem.swift; sourceTree = "<group>"; }; D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsVCardItem.swift; sourceTree = "<group>"; };
D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAccountInfoItem.swift; sourceTree = "<group>"; }; D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAccountInfoItem.swift; sourceTree = "<group>"; };
D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = "<group>"; }; D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = "<group>"; };
D0F69E801D6B8C850046BCD6 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = "<group>"; }; D0F69E801D6B8C850046BCD6 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = "<group>"; };
D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSwResample.h; sourceTree = "<group>"; }; D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSwResample.h; sourceTree = "<group>"; };
@@ -956,13 +958,6 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
D003702C1DA43006004308D3 /* Components */ = {
isa = PBXGroup;
children = (
);
name = Components;
sourceTree = "<group>";
};
D00C7CDA1E3776CA0080C3D5 /* Secret Preview */ = { D00C7CDA1E3776CA0080C3D5 /* Secret Preview */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1479,11 +1474,12 @@
D0C932391E0B4AC60074F044 /* Settings */ = { D0C932391E0B4AC60074F044 /* Settings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D01B279A1E39386C0022A4C0 /* SettingsControllerEntries.swift */, D01B279A1E39386C0022A4C0 /* SettingsController.swift */,
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */,
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */,
D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */,
D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */, D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */,
D0CE1BD21E51BC6100404327 /* DebugController.swift */,
D03E5E081E55C49C0029569A /* DebugAccountsController.swift */,
); );
name = Settings; name = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1686,9 +1682,12 @@
D00C7CD61E3664070080C3D5 /* ItemListMultilineInputItem.swift */, D00C7CD61E3664070080C3D5 /* ItemListMultilineInputItem.swift */,
D00B3F9D1E3A4847003872C3 /* ItemListSectionHeaderItem.swift */, D00B3F9D1E3A4847003872C3 /* ItemListSectionHeaderItem.swift */,
D00B3FA11E3A983E003872C3 /* ItemListTextItem.swift */, D00B3FA11E3A983E003872C3 /* ItemListTextItem.swift */,
D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */,
D021E0A81E3AACA200AF709C /* ItemListEditableItem.swift */, D021E0A81E3AACA200AF709C /* ItemListEditableItem.swift */,
D021E0AA1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift */, D021E0AA1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift */,
D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */,
D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */,
D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */,
); );
name = Items; name = Items;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1711,7 +1710,6 @@
D0EE97131D88BB1A006C18E1 /* Peer Info */ = { D0EE97131D88BB1A006C18E1 /* Peer Info */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D003702C1DA43006004308D3 /* Components */,
D0EE97141D88BB39006C18E1 /* Controller */, D0EE97141D88BB39006C18E1 /* Controller */,
); );
name = "Peer Info"; name = "Peer Info";
@@ -1725,6 +1723,10 @@
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */, D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */,
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */,
D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */,
D0486F091E523C8500091F0C /* GroupInfoController.swift */,
D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */,
D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */,
D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */,
); );
name = Controller; name = Controller;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1884,13 +1886,6 @@
D04BB2B71E44E5CB00650E93 /* Phone Entry */, D04BB2B71E44E5CB00650E93 /* Phone Entry */,
D04BB2BC1E44FD1300650E93 /* Code Entry */, D04BB2BC1E44FD1300650E93 /* Code Entry */,
D04BB2C11E45016800650E93 /* Password Entry */, D04BB2C11E45016800650E93 /* Password Entry */,
D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */,
D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */,
D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */,
D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */,
D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */,
D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */,
D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */,
); );
name = Authorization; name = Authorization;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2395,8 +2390,10 @@
D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */,
D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */, D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */,
D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */, D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */,
D03E5E0F1E55F8B90029569A /* ChannelVisibilityController.swift in Sources */,
D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */, D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */,
D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */, D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */,
D0561DE61E57424700E6B9E9 /* ItemListMultilineTextItem.swift in Sources */,
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */,
D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */,
D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */,
@@ -2417,7 +2414,6 @@
D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */, D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */,
D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */, D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */,
D0DE77001D92F1EB002B8809 /* ChatTitleView.swift in Sources */, D0DE77001D92F1EB002B8809 /* ChatTitleView.swift in Sources */,
D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */,
D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */, D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */,
D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */, D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */,
D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */,
@@ -2483,6 +2479,7 @@
D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */, D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */,
D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */, D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */,
D0215D561E043020001A0B1E /* InstantPageControllerNode.swift in Sources */, D0215D561E043020001A0B1E /* InstantPageControllerNode.swift in Sources */,
D0B98E7F1E575D2C008084B1 /* ChannelBlacklistController.swift in Sources */,
D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */,
D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */,
D0D03B121DECB0FE00220C46 /* bitwise.c in Sources */, D0D03B121DECB0FE00220C46 /* bitwise.c in Sources */,
@@ -2566,6 +2563,7 @@
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */,
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */, D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */,
D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */,
D03E5E091E55C49C0029569A /* DebugAccountsController.swift in Sources */,
D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */, D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */,
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */, D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */,
@@ -2591,15 +2589,14 @@
D0F917B51E0DA396003687E6 /* GenerateTextEntities.swift in Sources */, D0F917B51E0DA396003687E6 /* GenerateTextEntities.swift in Sources */,
D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */, D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */,
D04BB33E1E48797500650E93 /* platform_log.c in Sources */, D04BB33E1E48797500650E93 /* platform_log.c in Sources */,
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */, D01B279B1E39386C0022A4C0 /* SettingsController.swift in Sources */,
D01B279B1E39386C0022A4C0 /* SettingsControllerEntries.swift in Sources */,
D08775201E3F595000A97350 /* ContactListActionItem.swift in Sources */, D08775201E3F595000A97350 /* ContactListActionItem.swift in Sources */,
D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */, D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */,
D01B27991E39144C0022A4C0 /* ItemListController.swift in Sources */, D01B27991E39144C0022A4C0 /* ItemListController.swift in Sources */,
D0561DE11E57153000E6B9E9 /* ItemListActivityTextItem.swift in Sources */,
D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */, D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */,
D00370301DA43077004308D3 /* ItemListItem.swift in Sources */, D00370301DA43077004308D3 /* ItemListItem.swift in Sources */,
D0215D381E040F53001A0B1E /* InstantPageNode.swift in Sources */, D0215D381E040F53001A0B1E /* InstantPageNode.swift in Sources */,
D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */,
D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */,
D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */, D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */,
D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */,
@@ -2611,12 +2608,10 @@
D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */, D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */,
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */, D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */,
D04BB36F1E48797500650E93 /* RMGeometry.m in Sources */, D04BB36F1E48797500650E93 /* RMGeometry.m in Sources */,
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */,
D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */,
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */, D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */,
D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */, D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */,
D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */, D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */,
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */,
D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */, D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */,
D023837E1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift in Sources */, D023837E1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift in Sources */,
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */, D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */,
@@ -2625,7 +2620,7 @@
D04BB3381E48797500650E93 /* rngs.c in Sources */, D04BB3381E48797500650E93 /* rngs.c in Sources */,
D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */,
D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */, D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */,
D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */, D0486F0A1E523C8500091F0C /* GroupInfoController.swift in Sources */,
D04BB33C1E48797500650E93 /* timing.c in Sources */, D04BB33C1E48797500650E93 /* timing.c in Sources */,
D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */, D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */,
D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */, D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */,
@@ -2643,12 +2638,14 @@
D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */,
D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */, D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */,
D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */, D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */,
D0561DDF1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift in Sources */,
D01749551E1082770057C89A /* StoredMessageFromSearchPeer.swift in Sources */, D01749551E1082770057C89A /* StoredMessageFromSearchPeer.swift in Sources */,
D04BB32C1E48797500650E93 /* animations.c in Sources */, D04BB32C1E48797500650E93 /* animations.c in Sources */,
D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */, D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */,
D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */, D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */,
D0215D581E04302E001A0B1E /* InstantPageTileNode.swift in Sources */, D0215D581E04302E001A0B1E /* InstantPageTileNode.swift in Sources */,
D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */, D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */,
D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */,
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */,
D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */, D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */,
D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */,
@@ -2667,11 +2664,9 @@
D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */, D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */,
D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */,
D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */, D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */,
D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */,
D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */, D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */,
D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */,
D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */, D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */,
D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */,
D017494E1E1059570057C89A /* StringWithAppliedEntities.swift in Sources */, D017494E1E1059570057C89A /* StringWithAppliedEntities.swift in Sources */,
D0DE77231D932043002B8809 /* PeerMediaCollectionInterfaceState.swift in Sources */, D0DE77231D932043002B8809 /* PeerMediaCollectionInterfaceState.swift in Sources */,
D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */, D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */,
@@ -2679,6 +2674,7 @@
D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */, D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */,
D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */, D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */,
D03120F61DA534C1006A2A60 /* ItemListActionItem.swift in Sources */, D03120F61DA534C1006A2A60 /* ItemListActionItem.swift in Sources */,
D0CE1BD31E51BC6100404327 /* DebugController.swift in Sources */,
D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */, D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */,
D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */, D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */,
D0DC35441DE32230000195EB /* ChatInterfaceStateContextQueries.swift in Sources */, D0DC35441DE32230000195EB /* ChatInterfaceStateContextQueries.swift in Sources */,

View File

@@ -1,66 +0,0 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKitDynamic
import TelegramCore
enum AuthorizationCodeResult {
case Authorization(Api.auth.Authorization)
case Password(String)
}
class AuthorizationCodeController: ViewController {
let account: UnauthorizedAccount
let phone: String
let sentCode: Api.auth.SentCode
var node: AuthorizationCodeControllerNode {
return self.displayNode as! AuthorizationCodeControllerNode
}
let signInDisposable = MetaDisposable()
let resultPipe = ValuePipe<AuthorizationCodeResult>()
var result: Signal<AuthorizationCodeResult, NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount, phone: String, sentCode: Api.auth.SentCode) {
self.account = account
self.phone = phone
self.sentCode = sentCode
super.init()
self.title = "Code"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationCodeController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.signInDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationCodeControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
@objc func nextPressed() {
var phoneCodeHash: String?
switch self.sentCode {
case let .sentCode(_, _, apiPhoneCodeHash, _, _):
phoneCodeHash = apiPhoneCodeHash
default:
break
}
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationCodeControllerNode: ASDisplayNode {
let codeNode: ASEditableTextNode
override init() {
self.codeNode = ASEditableTextNode()
super.init()
self.codeNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.codeNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.codeNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.codeNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

View File

@@ -1,86 +0,0 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
public class AuthorizationController: NavigationController {
private var account: UnauthorizedAccount!
private let authorizedAccountValue = Promise<Account>()
public var authorizedAccount: Signal<Account, NoError> {
return authorizedAccountValue.get()
}
public init(account: UnauthorizedAccount) {
self.account = account
let phoneController = AuthorizationPhoneController(account: account)
super.init()
self.pushViewController(phoneController, animated: false)
let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
return deferred { [weak self] in
if let strongSelf = self {
strongSelf.account = account
let codeController = AuthorizationCodeController(account: account, phone: phone, sentCode: sentCode)
strongSelf.pushViewController(codeController, animated: true)
return codeController.result |> mapToSignal { result -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
switch result {
case let .Authorization(authorization):
return single((authorization, account), NoError.self)
case .Password:
return deferred { [weak self] () -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
if let strongSelf = self {
let passwordController = AuthorizationPasswordController(account: account)
strongSelf.pushViewController(passwordController, animated: true)
return passwordController.result |> map { ($0, account) }
} else {
return .complete()
}
} |> runOn(Queue.mainQueue())
}
}
} else {
return .complete()
}
} |> runOn(Queue.mainQueue())
}
let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization, account -> Signal<Account, NoError> in
if let strongSelf = self {
switch authorization {
case let .authorization(_, _, user):
let user = TelegramUser(user: user)
return account.postbox.modify { modifier -> AccountState in
let state = AuthorizedAccountState(masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil)
modifier.setState(state)
return state
} |> map { state -> Account in
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
}
}
} else {
return .complete()
}
}
self.authorizedAccountValue.set(accountSignal)
}
override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
}

View File

@@ -1,57 +0,0 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKitDynamic
import TelegramCore
class AuthorizationPasswordController: ViewController {
private var account: UnauthorizedAccount
private var node: AuthorizationPasswordControllerNode {
return self.displayNode as! AuthorizationPasswordControllerNode
}
private let signInDisposable = MetaDisposable()
private let resultPipe = ValuePipe<Api.auth.Authorization>()
var result: Signal<Api.auth.Authorization, NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount) {
self.account = account
super.init()
self.title = "Password"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationPasswordController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
signInDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationPasswordControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
@objc func nextPressed() {
let password = self.node.passwordNode.attributedText?.string ?? ""
self.signInDisposable.set(verifyPassword(self.account, password: password).start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.resultPipe.putNext(result)
}
}))
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationPasswordControllerNode: ASDisplayNode {
let passwordNode: ASEditableTextNode
override init() {
self.passwordNode = ASEditableTextNode()
super.init()
self.passwordNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.passwordNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.passwordNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.passwordNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

View File

@@ -1,81 +0,0 @@
import Foundation
import Display
import SwiftSignalKit
import MtProtoKitDynamic
import TelegramCore
class AuthorizationPhoneController: ViewController {
private var account: UnauthorizedAccount
private var node: AuthorizationPhoneControllerNode {
return self.displayNode as! AuthorizationPhoneControllerNode
}
private let codeDisposable = MetaDisposable()
private let resultPipe = ValuePipe<(UnauthorizedAccount, Api.auth.SentCode, String)>()
var result: Signal<(UnauthorizedAccount, Api.auth.SentCode, String), NoError> {
return resultPipe.signal()
}
init(account: UnauthorizedAccount) {
self.account = account
super.init()
self.title = "Telegram"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(AuthorizationPhoneController.nextPressed))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
codeDisposable.dispose()
}
override func loadDisplayNode() {
self.displayNode = AuthorizationPhoneControllerNode()
self.displayNodeDidLoad()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
@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")
let signal = account.network.request(sendCode)
|> 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)
|> map { sentCode in
return (sentCode, updatedAccount)
}
}
case _:
return .fail(error)
}
}
codeDisposable.set(signal.start(next: { [weak self] (result, account) in
if let strongSelf = self {
strongSelf.account = account
strongSelf.resultPipe.putNext((account, result, phone))
}
}))
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
import Display
import AsyncDisplayKit
class AuthorizationPhoneControllerNode: ASDisplayNode {
let phoneNode: ASEditableTextNode
override init() {
self.phoneNode = ASEditableTextNode()
super.init()
self.phoneNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.phoneNode.backgroundColor = UIColor.lightGray
self.addSubnode(self.phoneNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.phoneNode.frame = CGRect(origin: CGPoint(x: 4.0, y: navigationBarHeight + 4.0), size: CGSize(width: layout.size.width - 8.0, height: 32.0))
}
}

View File

@@ -77,60 +77,26 @@ public final class AuthorizationSequenceController: NavigationController {
controller.loginWithNumber = { [weak self, weak controller] number in controller.loginWithNumber = { [weak self, weak controller] number in
if let strongSelf = self { if let strongSelf = self {
controller?.inProgress = true controller?.inProgress = true
let account = strongSelf.account strongSelf.actionDisposable.set((sendAuthorizationCode(account: strongSelf.account, phoneNumber: number, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb") |> deliverOnMainQueue).start(next: { [weak self] account in
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 { if let strongSelf = self {
controller?.inProgress = false
strongSelf.account = account 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 }, error: { error in
Queue.mainQueue().async { if let controller = controller {
if let controller = controller { controller.inProgress = false
controller.inProgress = false
let text: String
var text: String? switch error {
if error.errorDescription.hasPrefix("FLOOD_WAIT") { case .limitExceeded:
text = "You have requested authorization code too many times. Please try again later." text = "You have requested authorization code too many times. Please try again later."
} else if error.errorDescription == "PHONE_NUMBER_INVALID" { case .invalidPhoneNumber:
text = "The phone number you entered is not valid. Please enter the correct number along with your area code." text = "The phone number you entered is not valid. Please enter the correct number along with your area code."
} case .generic:
if let text = text { text = "An error occurred. Please try again later."
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
}
} }
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
} }
})) }))
} }
@@ -157,76 +123,22 @@ public final class AuthorizationSequenceController: NavigationController {
if let strongSelf = self { if let strongSelf = self {
controller?.inProgress = true controller?.inProgress = true
let account = strongSelf.account strongSelf.actionDisposable.set((authorizeWithCode(account: strongSelf.account, code: code) |> deliverOnMainQueue).start(error: { error in
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 { Queue.mainQueue().async {
if let controller = controller { if let controller = controller {
controller.inProgress = false controller.inProgress = false
var text: String? let text: String
if error.hasPrefix("FLOOD_WAIT") { switch error {
text = "You have entered invalid code too many times. Please try again later." case .limitExceeded:
} else if error == "CODE_INVALID" { text = "You have entered invalid code too many times. Please try again later."
text = "Invalid code." case .invalidCode:
} else { text = "Invalid code. Please try again."
text = "An error occured."; case .generic:
} 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.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
} }
} }
})) }))
@@ -254,40 +166,22 @@ public final class AuthorizationSequenceController: NavigationController {
if let strongSelf = self { if let strongSelf = self {
controller?.inProgress = true controller?.inProgress = true
let account = strongSelf.account strongSelf.actionDisposable.set((authorizeWithPassword(account: strongSelf.account, password: password) |> deliverOnMainQueue).start(error: { error in
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 { Queue.mainQueue().async {
if let controller = controller { if let controller = controller {
controller.inProgress = false controller.inProgress = false
var text: String? let text: String
if error.hasPrefix("FLOOD_WAIT") { switch error {
text = "You have entered invalid password too many times. Please try again later." case .limitExceeded:
} else if error == "PASSWORD_HASH_INVALID" { text = "You have entered invalid password too many times. Please try again later."
text = "Invalid password." case .invalidPassword:
} else { text = "Invalid password. Please try again."
text = "An error occured."; case .generic:
} text = "An error occured. Please try again later."
if let text = text {
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
} }
controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window)
} }
} }
})) }))
@@ -314,7 +208,7 @@ public final class AuthorizationSequenceController: NavigationController {
self.setViewControllers([self.splashController(), self.passwordEntryController(hint: hint)], animated: !self.viewControllers.isEmpty) self.setViewControllers([self.splashController(), self.passwordEntryController(hint: hint)], animated: !self.viewControllers.isEmpty)
} }
} else if let _ = state as? AuthorizedAccountState { } 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 self._authorizedAccount.set(accountWithId(self.account.id, appGroupPath: self.account.appGroupPath, testingEnvironment: self.account.testingEnvironment) |> mapToSignal { account -> Signal<Account, NoError> in
if case let .right(authorizedAccount) = account { if case let .right(authorizedAccount) = account {
return .single(authorizedAccount) return .single(authorizedAccount)
} else { } else {

View File

@@ -42,6 +42,10 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
self.displayNode = AuthorizationSequencePasswordEntryControllerNode() self.displayNode = AuthorizationSequencePasswordEntryControllerNode()
self.displayNodeDidLoad() self.displayNodeDidLoad()
self.controllerNode.loginWithCode = { [weak self] _ in
self?.nextPressed()
}
if let hint = self.hint { if let hint = self.hint {
self.controllerNode.updateData(hint: hint) self.controllerNode.updateData(hint: hint)
} }

View File

@@ -2,7 +2,7 @@ import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode { final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UITextFieldDelegate {
private let navigationBackgroundNode: ASDisplayNode private let navigationBackgroundNode: ASDisplayNode
private let stripeNode: ASDisplayNode private let stripeNode: ASDisplayNode
private let titleNode: ASTextNode private let titleNode: ASTextNode
@@ -67,6 +67,8 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode {
self.backgroundColor = UIColor.white self.backgroundColor = UIColor.white
self.codeField.textField.delegate = self
self.addSubnode(self.navigationBackgroundNode) self.addSubnode(self.navigationBackgroundNode)
self.addSubnode(self.stripeNode) self.addSubnode(self.stripeNode)
self.addSubnode(self.codeSeparatorNode) self.addSubnode(self.codeSeparatorNode)
@@ -171,4 +173,9 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode {
@objc func passwordFieldTextChanged(_ textField: UITextField) { @objc func passwordFieldTextChanged(_ textField: UITextField) {
} }
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.loginWithCode?(self.currentPassword)
return false
}
} }

View File

@@ -84,8 +84,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController {
} }
@objc func nextPressed() { @objc func nextPressed() {
let (code, number) = self.controllerNode.codeAndNumber let (_, number) = self.controllerNode.codeAndNumber
if code != nil && !number.isEmpty { if !number.isEmpty {
self.loginWithNumber?(self.controllerNode.currentNumber) self.loginWithNumber?(self.controllerNode.currentNumber)
} else { } else {
hapticFeedback.error() hapticFeedback.error()

View File

@@ -0,0 +1,384 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed()
private struct ChannelAdminsControllerArguments {
let account: Account
let updateCurrentAdministrationType: () -> Void
let addAdmin: () -> Void
}
private enum ChannelAdminsSection: Int32 {
case administration
case admins
}
private enum ChannelAdminsEntryStableId: Hashable {
case index(Int32)
case peer(PeerId)
var hashValue: Int {
switch self {
case let .index(index):
return index.hashValue
case let .peer(peerId):
return peerId.hashValue
}
}
static func ==(lhs: ChannelAdminsEntryStableId, rhs: ChannelAdminsEntryStableId) -> Bool {
switch lhs {
case let .index(index):
if case .index(index) = rhs {
return true
} else {
return false
}
case let .peer(peerId):
if case .peer(peerId) = rhs {
return true
} else {
return false
}
}
}
}
private enum ChannelAdminsEntry: ItemListNodeEntry {
case administrationType(CurrentAdministrationType)
case administrationInfo(String)
case adminsHeader(String)
case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing)
case addAdmin(Bool)
case adminsInfo(String)
var section: ItemListSectionId {
switch self {
case .administrationType, .administrationInfo:
return ChannelAdminsSection.administration.rawValue
case .adminsHeader, .adminPeerItem, .addAdmin, .adminsInfo:
return ChannelAdminsSection.admins.rawValue
}
}
var stableId: ChannelAdminsEntryStableId {
switch self {
case .administrationType:
return .index(0)
case .administrationInfo:
return .index(1)
case .adminsHeader:
return .index(2)
case .addAdmin:
return .index(3)
case .adminsInfo:
return .index(4)
case let .adminPeerItem(_, participant, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool {
switch lhs {
case let .administrationType(type):
if case .administrationType(type) = rhs {
return true
} else {
return false
}
case let .administrationInfo(text):
if case .administrationInfo(text) = rhs {
return true
} else {
return false
}
case let .adminsHeader(title):
if case .adminsHeader(title) = rhs {
return true
} else {
return false
}
case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing):
if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsParticipant != rhsParticipant {
return false
}
if lhsEditing != rhsEditing {
return false
}
return true
} else {
return false
}
case let .adminsInfo(text):
if case .adminsInfo(text) = rhs {
return true
} else {
return false
}
case let .addAdmin(editing):
if case .addAdmin(editing) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool {
switch lhs {
case .administrationType:
return true
case .administrationInfo:
switch rhs {
case .administrationType:
return false
default:
return true
}
case .adminsHeader:
switch rhs {
case .administrationType, .administrationInfo:
return false
default:
return true
}
case let .adminPeerItem(index, _, _):
switch rhs {
case .administrationType, .administrationInfo, .adminsHeader:
return false
case let .adminPeerItem(rhsIndex, _, _):
return index < rhsIndex
default:
return true
}
case .addAdmin:
switch rhs {
case .administrationType, .administrationInfo, .adminsHeader, .adminPeerItem:
return false
default:
return true
}
case .adminsInfo:
return false
}
}
func item(_ arguments: ChannelAdminsControllerArguments) -> ListViewItem {
switch self {
case let .administrationType(type):
let label: String
switch type {
case .adminsCanAddMembers:
label = "Only Admins"
case .everyoneCanAddMembers:
label = "All Members"
}
return ItemListDisclosureItem(title: "Who can add members", label: label, sectionId: self.section, style: .blocks, action: {
})
case let .administrationInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
case let .adminsHeader(title):
return ItemListSectionHeaderItem(text: title, sectionId: self.section)
case let .adminPeerItem(_, participant, editing):
let peerText: String
switch participant.participant {
case .creator:
peerText = "Creator"
default:
peerText = "Moderator"
}
return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: nil, editing: editing, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
}, removePeer: { _ in
})
case let .addAdmin(editing):
return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Admin", sectionId: self.section, editing: editing, action: {
arguments.addAdmin()
})
case let .adminsInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
}
}
}
private enum CurrentAdministrationType {
case everyoneCanAddMembers
case adminsCanAddMembers
}
private struct ChannelAdminsControllerState: Equatable {
let selectedType: CurrentAdministrationType?
init() {
self.selectedType = nil
}
init(selectedType: CurrentAdministrationType?) {
self.selectedType = selectedType
}
static func ==(lhs: ChannelAdminsControllerState, rhs: ChannelAdminsControllerState) -> Bool {
if lhs.selectedType != rhs.selectedType {
return false
}
return true
}
func withUpdatedSelectedType(_ selectedType: CurrentAdministrationType?) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: selectedType)
}
}
private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelAdminsEntry] {
var entries: [ChannelAdminsEntry] = []
if let peer = view.peers[view.peerId] as? TelegramChannel {
var isGroup = false
if case let .group(info) = peer.info {
isGroup = true
let selectedType: CurrentAdministrationType
if let current = state.selectedType {
selectedType = current
} else {
if info.flags.contains(.everyMemberCanInviteMembers) {
selectedType = .everyoneCanAddMembers
} else {
selectedType = .adminsCanAddMembers
}
}
entries.append(.administrationType(selectedType))
let infoText: String
switch selectedType {
case .everyoneCanAddMembers:
infoText = "Everybody can add new members"
case .adminsCanAddMembers:
infoText = "Only Admins can add new mebers"
}
entries.append(.administrationInfo(infoText))
}
if let participants = participants {
entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS"))
var index: Int32 = 0
for participant in participants.sorted(by: { lhs, rhs in
let lhsInvitedAt: Int32
switch lhs.participant {
case .creator:
lhsInvitedAt = Int32.min
case let .editor(_, _, invitedAt):
lhsInvitedAt = invitedAt
case let .moderator(_, _, invitedAt):
lhsInvitedAt = invitedAt
case let .member(_, invitedAt):
lhsInvitedAt = invitedAt
}
let rhsInvitedAt: Int32
switch rhs.participant {
case .creator:
rhsInvitedAt = Int32.min
case let .editor(_, _, invitedAt):
rhsInvitedAt = invitedAt
case let .moderator(_, _, invitedAt):
rhsInvitedAt = invitedAt
case let .member(_, invitedAt):
rhsInvitedAt = invitedAt
}
return lhsInvitedAt < rhsInvitedAt
}) {
var editable = true
if case .creator = participant.participant {
editable = false
}
entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: false, revealed: false)))
index += 1
}
entries.append(.addAdmin(false))
entries.append(.adminsInfo(isGroup ? "You can add admins to help you manage your group" : "You can add admins to help you manage your channel"))
}
}
return entries
}
/*private func effectiveAdministrationType(state: ChannelAdminsControllerState, peer: TelegramChannel) -> CurrentAdministrationType {
let selectedType: CurrentAdministrationType
if let current = state.selectedType {
selectedType = current
} else {
if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
}
return selectedType
}*/
public func ChannelAdminsController(account: Account, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelAdminsControllerState())
let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let actionsDisposable = DisposableSet()
let updateAdministrationDisposable = MetaDisposable()
actionsDisposable.add(updateAdministrationDisposable)
let addAdminDisposable = MetaDisposable()
actionsDisposable.add(addAdminDisposable)
let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: {
}, addAdmin: {
})
let peerView = account.viewTracker.peerView(peerId)
let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil)
let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) })
adminsPromise.set(adminsSignal)
let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get())
|> map { state, view, admins -> (ItemListControllerState, (ItemListNodeState<ChannelAdminsEntry>, ChannelAdminsEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
var rightNavigationButton: ItemListNavigationButton?
if let admins = admins, admins.count > 1 {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
updateState { state in
return state
}
})
}
let controllerState = ItemListControllerState(title: "Admins", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
let listState = ItemListNodeState(entries: ChannelAdminsControllerEntries(view: view, state: state, participants: admins), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
return controller
}

View File

@@ -0,0 +1,2 @@
import Foundation

View File

@@ -1,4 +1,4 @@
import Foundation /*import Foundation
import Postbox import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
@@ -234,4 +234,4 @@ func channelBroadcastInfoEntries(view: PeerView) -> PeerInfoEntries {
} }
} }
return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil) return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil)
} }*/

View File

@@ -0,0 +1,555 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private struct ChannelVisibilityControllerArguments {
let account: Account
let updateCurrentType: (CurrentChannelType) -> Void
let updatePublicLinkText: (String) -> Void
let displayPrivateLinkMenu: () -> Void
}
private enum ChannelVisibilitySection: Int32 {
case type
case link
case existingPublicLinks
}
private enum ChannelVisibilityEntry: ItemListNodeEntry {
case typeHeader(String)
case typePublic(Bool)
case typePrivate(Bool)
case typeInfo(String)
case privateLink(String?)
case editablePublicLink(String)
case privateLinkInfo(String)
case publicLinkInfo(String)
case publicLinkStatus(String, AddressNameStatus)
case existingLinksInfo(String)
case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing)
var section: ItemListSectionId {
switch self {
case .typeHeader, .typePublic, .typePrivate, .typeInfo:
return ChannelVisibilitySection.type.rawValue
case .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
return ChannelVisibilitySection.link.rawValue
case .existingLinksInfo, .existingLinkPeerItem:
return ChannelVisibilitySection.existingPublicLinks.rawValue
}
}
var stableId: Int32 {
switch self {
case .typeHeader:
return 0
case .typePublic:
return 1
case .typePrivate:
return 2
case .typeInfo:
return 3
case .privateLink:
return 4
case .editablePublicLink:
return 5
case .privateLinkInfo:
return 6
case .publicLinkStatus:
return 7
case .publicLinkInfo:
return 8
case .existingLinksInfo:
return 9
case let .existingLinkPeerItem(index, _, _):
return 10 + index
}
}
static func ==(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool {
switch lhs {
case let .typeHeader(title):
if case .typeHeader(title) = rhs {
return true
} else {
return false
}
case let .typePublic(selected):
if case .typePublic(selected) = rhs {
return true
} else {
return false
}
case let .typePrivate(selected):
if case .typePrivate(selected) = rhs {
return true
} else {
return false
}
case let .typeInfo(text):
if case .typeInfo(text) = rhs {
return true
} else {
return false
}
case let .privateLink(lhsLink):
if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink {
return true
} else {
return false
}
case let .editablePublicLink(text):
if case .editablePublicLink(text) = rhs {
return true
} else {
return false
}
case let .privateLinkInfo(text):
if case .privateLinkInfo(text) = rhs {
return true
} else {
return false
}
case let .publicLinkInfo(text):
if case .publicLinkInfo(text) = rhs {
return true
} else {
return false
}
case let .publicLinkStatus(addressName, status):
if case .publicLinkStatus(addressName, status) = rhs {
return true
} else {
return false
}
case let .existingLinksInfo(text):
if case .existingLinksInfo(text) = rhs {
return true
} else {
return false
}
case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing):
if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing) = rhs {
if lhsIndex != rhsIndex {
return false
}
if !lhsPeer.isEqual(rhsPeer) {
return false
}
if lhsEditing != rhsEditing {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: ChannelVisibilityControllerArguments) -> ListViewItem {
switch self {
case let .typeHeader(title):
return ItemListSectionHeaderItem(text: title, sectionId: self.section)
case let .typePublic(selected):
return ItemListCheckboxItem(title: "Public", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateCurrentType(.publicChannel)
})
case let .typePrivate(selected):
return ItemListCheckboxItem(title: "Private", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateCurrentType(.privateChannel)
})
case let .typeInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
case let .privateLink(link):
return ItemListActionItem(title: link ?? "Loading", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
})
case let .editablePublicLink(text):
return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(updatedText)
}, action: {
})
case let .privateLinkInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
case let .publicLinkInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
case let .publicLinkStatus(addressName, status):
var displayActivity = false
let text: NSAttributedString
switch status {
case .available:
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c))
case .checking:
text = NSAttributedString(string: "Checking name...", textColor: .gray)
displayActivity = true
case let .invalid(reason):
switch reason {
case .alreadyTaken:
text = NSAttributedString(string: "\(addressName) is already taken.", textColor: .red)
case .digitStart:
text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030))
case .invalid, .underscopeEnd, .underscopeStart:
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
case .short:
text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030))
}
}
return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section)
case let .existingLinksInfo(text):
return ItemListTextItem(text: text, sectionId: self.section)
case let .existingLinkPeerItem(_, peer, editing):
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .activity, label: nil, editing: editing, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
}, removePeer: { _ in
})
}
}
}
private enum CurrentChannelType {
case publicChannel
case privateChannel
}
private enum AddressNameStatus: Equatable {
case available
case checking
case invalid(UsernameAvailabilityError)
static func ==(lhs: AddressNameStatus, rhs: AddressNameStatus) -> Bool {
switch lhs {
case .available:
if case .available = rhs {
return true
} else {
return false
}
case .checking:
if case .checking = rhs {
return true
} else {
return false
}
case let .invalid(reason):
if case .invalid(reason) = rhs {
return true
} else {
return false
}
}
}
}
private struct ChannelVisibilityControllerState: Equatable {
let selectedType: CurrentChannelType?
let editingPublicLinkText: String?
let addressNameStatus: AddressNameStatus?
let updatingAddressName: Bool
init() {
self.selectedType = nil
self.editingPublicLinkText = nil
self.addressNameStatus = nil
self.updatingAddressName = false
}
init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameStatus: AddressNameStatus?, updatingAddressName: Bool) {
self.selectedType = selectedType
self.editingPublicLinkText = editingPublicLinkText
self.addressNameStatus = addressNameStatus
self.updatingAddressName = updatingAddressName
}
static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool {
if lhs.selectedType != rhs.selectedType {
return false
}
if lhs.editingPublicLinkText != rhs.editingPublicLinkText {
return false
}
if lhs.addressNameStatus != rhs.addressNameStatus {
return false
}
if lhs.updatingAddressName != rhs.updatingAddressName {
return false
}
return true
}
func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName)
}
func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName)
}
func withUpdatedAddressNameStatus(_ addressNameStatus: AddressNameStatus?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: addressNameStatus, updatingAddressName: self.updatingAddressName)
}
func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: updatingAddressName)
}
}
private func channelVisibilityControllerEntries(view: PeerView, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] {
var entries: [ChannelVisibilityEntry] = []
if let peer = view.peers[view.peerId] as? TelegramChannel {
var isGroup = false
if case .group = peer.info {
isGroup = true
}
let selectedType: CurrentChannelType
if let current = state.selectedType {
selectedType = current
} else {
if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
}
let currentAddressName: String
if let current = state.editingPublicLinkText {
currentAddressName = current
} else {
if let addressName = peer.addressName {
currentAddressName = addressName
} else {
currentAddressName = ""
}
}
entries.append(.typeHeader(isGroup ? "GROUP TYPE" : "CHANNEL TYPE"))
entries.append(.typePublic(selectedType == .publicChannel))
entries.append(.typePrivate(selectedType == .privateChannel))
switch selectedType {
case .publicChannel:
if isGroup {
entries.append(.typeInfo("Public groups can be found in search, chat history is available to everyone and anyone can join."))
} else {
entries.append(.typeInfo("Public channels can be found in search and anyone can join."))
}
case .privateChannel:
if isGroup {
entries.append(.typeInfo("Private groups can only be joined if you were invited of have an invite link."))
} else {
entries.append(.typeInfo("Private channels can only be joined if you were invited of have an invite link."))
}
}
switch selectedType {
case .publicChannel:
entries.append(.editablePublicLink(currentAddressName))
if let status = state.addressNameStatus {
entries.append(.publicLinkStatus(currentAddressName, status))
}
entries.append(.publicLinkInfo("People can share this link with others and find your group using Telegram search."))
case .privateChannel:
entries.append(.privateLink((view.cachedData as? CachedChannelData)?.exportedInvitation?.link))
entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time."))
}
}
return entries
}
private func effectiveChannelType(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> CurrentChannelType {
let selectedType: CurrentChannelType
if let current = state.selectedType {
selectedType = current
} else {
if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
}
return selectedType
}
private func updatedAddressName(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> String? {
let selectedType = effectiveChannelType(state: state, peer: peer)
let currentAddressName: String
switch selectedType {
case .privateChannel:
currentAddressName = ""
case .publicChannel:
if let current = state.editingPublicLinkText {
currentAddressName = current
} else {
if let addressName = peer.addressName {
currentAddressName = addressName
} else {
currentAddressName = ""
}
}
}
if !currentAddressName.isEmpty {
if currentAddressName != peer.addressName {
return currentAddressName
} else {
return nil
}
} else if peer.addressName != nil {
return ""
} else {
return nil
}
}
public func channelVisibilityController(account: Account, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelVisibilityControllerState())
let updateState: ((ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable)
let updateAddressNameDisposable = MetaDisposable()
actionsDisposable.add(updateAddressNameDisposable)
let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in
updateState { state in
return state.withUpdatedSelectedType(type)
}
}, updatePublicLinkText: { text in
if text.isEmpty {
checkAddressNameDisposable.set(nil)
updateState { state in
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameStatus(nil)
}
} else {
updateState { state in
return state.withUpdatedEditingPublicLinkText(text)
}
checkAddressNameDisposable.set((addressNameAvailability(account: account, domain: .peer(peerId), def: nil, current: text)
|> deliverOnMainQueue).start(next: { result in
updateState { state in
let status: AddressNameStatus
switch result {
case let .fail(_, error):
status = .invalid(error)
case .none:
status = .available
case .success:
status = .available
case .progress:
status = .checking
}
return state.withUpdatedAddressNameStatus(status)
}
}))
}
}, displayPrivateLinkMenu: {
})
let peerView = account.viewTracker.peerView(peerId)
let signal = combineLatest(statePromise.get(), peerView)
|> map { state, view -> (ItemListControllerState, (ItemListNodeState<ChannelVisibilityEntry>, ChannelVisibilityEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
var rightNavigationButton: ItemListNavigationButton?
if let peer = peer as? TelegramChannel {
var doneEnabled = true
if let selectedType = state.selectedType {
switch selectedType {
case .privateChannel:
break
case .publicChannel:
if let addressNameStatus = state.addressNameStatus {
switch addressNameStatus {
case .available:
break
default:
doneEnabled = false
}
}
}
}
rightNavigationButton = ItemListNavigationButton(title: "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: {
var updatedAddressNameValue: String?
updateState { state in
updatedAddressNameValue = updatedAddressName(state: state, peer: peer)
if updatedAddressNameValue != nil {
return state.withUpdatedUpdatingAddressName(true)
} else {
return state
}
}
if let updatedAddressNameValue = updatedAddressNameValue {
updateAddressNameDisposable.set((updatePeerAddressName(account: account, peerId: peerId, username: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedUpdatingAddressName(false)
}
}, completed: {
updateState { state in
return state.withUpdatedUpdatingAddressName(false)
}
dismissImpl?()
}))
} else {
dismissImpl?()
}
})
}
var isGroup = false
if let peer = peer as? TelegramChannel {
if case .group = peer.info {
isGroup = true
}
}
let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: {
dismissImpl?()
})
let controllerState = ItemListControllerState(title: isGroup ? "Group Type" : "Channel Link", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false)
let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(view: view, state: state), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

View File

@@ -133,6 +133,7 @@ public class ChatController: TelegramController {
} }
})) }))
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in
if let strongSelf = self { if let strongSelf = self {
var transitionNode: ASDisplayNode? var transitionNode: ASDisplayNode?
@@ -1029,7 +1030,7 @@ public class ChatController: TelegramController {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.chatDisplayNode.historyNode.preloadPages = true self.chatDisplayNode.historyNode.preloadPages = true
self.chatDisplayNode.historyNode.canReadHistory.set(true) self.chatDisplayNode.historyNode.canReadHistory.set((self.account.applicationContext as! TelegramApplicationContext).applicationInForeground)
self.chatDisplayNode.loadInputPanels() self.chatDisplayNode.loadInputPanels()
@@ -1041,11 +1042,11 @@ public class ChatController: TelegramController {
override public func viewWillDisappear(_ animated: Bool) { override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
self.chatDisplayNode.historyNode.canReadHistory.set(false) self.chatDisplayNode.historyNode.canReadHistory.set(.single(false))
let peerId = self.peerId let peerId = self.peerId
let timestamp = Int32(Date().timeIntervalSince1970) let timestamp = Int32(Date().timeIntervalSince1970)
let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp) let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
self.account.postbox.modify({ modifier -> Void in let _ = self.account.postbox.modify({ modifier -> Void in
modifier.updatePeerChatInterfaceState(peerId, update: { _ in modifier.updatePeerChatInterfaceState(peerId, update: { _ in
return interfaceState return interfaceState
}) })
@@ -1213,9 +1214,10 @@ public class ChatController: TelegramController {
self.navigationActionDisposable.set((self.peerView.get() self.navigationActionDisposable.set((self.peerView.get()
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self] peerView in |> deliverOnMainQueue).start(next: { [weak self] peerView in
if let strongSelf = self, let _ = peerView.peers[peerView.peerId] { if let strongSelf = self, let peer = peerView.peers[peerView.peerId] {
let chatInfoController = PeerInfoController(account: strongSelf.account, peerId: peerView.peerId) if let infoController = peerInfoController(account: strongSelf.account, peer: peer) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController) (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
}
} }
})) }))
break break

View File

@@ -193,7 +193,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
private let maxVisibleIncomingMessageIndex = ValuePromise<MessageIndex>(ignoreRepeated: true) private let maxVisibleIncomingMessageIndex = ValuePromise<MessageIndex>(ignoreRepeated: true)
let canReadHistory = ValuePromise<Bool>() let canReadHistory = Promise<Bool>()
private let _chatHistoryLocation = ValuePromise<ChatHistoryLocation>() private let _chatHistoryLocation = ValuePromise<ChatHistoryLocation>()
private var chatHistoryLocation: Signal<ChatHistoryLocation, NoError> { private var chatHistoryLocation: Signal<ChatHistoryLocation, NoError> {

View File

@@ -31,5 +31,5 @@ public protocol ChatHistoryNode: class {
func messageInCurrentHistoryView(_ id: MessageId) -> Message? func messageInCurrentHistoryView(_ id: MessageId) -> Message?
func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets)
func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) func forEachItemNode(_ f: (ASDisplayNode) -> Void)
} }

View File

@@ -169,7 +169,7 @@ class ChatMessageActionItemNode: ChatMessageItemView {
} }
} }
let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil) let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), nil)
let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0)
var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)

View File

@@ -6,7 +6,7 @@ import SwiftSignalKit
private let dateFont = UIFont.italicSystemFont(ofSize: 11.0) private let dateFont = UIFont.italicSystemFont(ofSize: 11.0)
private func generateCheckImage(partial: Bool) -> UIImage? { private func generateCheckImage(partial: Bool, color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in
context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0) context.scaleBy(x: 1.0, y: -1.0)
@@ -14,7 +14,7 @@ private func generateCheckImage(partial: Bool) -> UIImage? {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: 0.5, y: 0.5) context.scaleBy(x: 0.5, y: 0.5)
context.setStrokeColor(UIColor(0x19C700).cgColor) context.setStrokeColor(color.cgColor)
context.setLineWidth(2.5) context.setLineWidth(2.5)
if partial { if partial {
let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ") let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ")
@@ -25,11 +25,11 @@ private func generateCheckImage(partial: Bool) -> UIImage? {
}) })
} }
private func generateClockFrameImage() -> UIImage? { private func generateClockFrameImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0x42b649).cgColor) context.setStrokeColor(color.cgColor)
context.setFillColor(UIColor(0x42b649).cgColor) context.setFillColor(color.cgColor)
let strokeWidth: CGFloat = 1.0 let strokeWidth: CGFloat = 1.0
context.setLineWidth(strokeWidth) context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth)) context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth))
@@ -37,10 +37,10 @@ private func generateClockFrameImage() -> UIImage? {
}) })
} }
private func generateClockMinImage() -> UIImage? { private func generateClockMinImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x42b649).cgColor) context.setFillColor(color.cgColor)
let strokeWidth: CGFloat = 1.0 let strokeWidth: CGFloat = 1.0
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth)) context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth))
}) })
@@ -61,14 +61,21 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
layer.add(basicAnimation, forKey: "clockFrameAnimation") layer.add(basicAnimation, forKey: "clockFrameAnimation")
} }
private let checkFullImage = generateCheckImage(partial: false) private let checkBubbleFullImage = generateCheckImage(partial: false, color: UIColor(0x19C700))
private let checkPartialImage = generateCheckImage(partial: true) private let checkBubblePartialImage = generateCheckImage(partial: true, color: UIColor(0x19C700))
private let checkMediaFullImage = generateCheckImage(partial: false, color: .white)
private let checkMediaPartialImage = generateCheckImage(partial: true, color: .white)
private let incomingDateColor = UIColor(0x525252, 0.6) private let incomingDateColor = UIColor(0x525252, 0.6)
private let outgoingDateColor = UIColor(0x008c09, 0.8) private let outgoingDateColor = UIColor(0x008c09, 0.8)
private let clockFrameImage = generateClockFrameImage() private let imageBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(white: 0.0, alpha: 0.5))
private let clockMinImage = generateClockMinImage()
private let clockBubbleFrameImage = generateClockFrameImage(color: UIColor(0x42b649))
private let clockBubbleMinImage = generateClockMinImage(color: UIColor(0x42b649))
private let clockMediaFrameImage = generateClockFrameImage(color: .white)
private let clockMediaMinImage = generateClockMinImage(color: .white)
enum ChatMessageDateAndStatusOutgoingType { enum ChatMessageDateAndStatusOutgoingType {
case Sent(read: Bool) case Sent(read: Bool)
@@ -79,9 +86,12 @@ enum ChatMessageDateAndStatusOutgoingType {
enum ChatMessageDateAndStatusType { enum ChatMessageDateAndStatusType {
case BubbleIncoming case BubbleIncoming
case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType)
case ImageIncoming
case ImageOutgoing(ChatMessageDateAndStatusOutgoingType)
} }
class ChatMessageDateAndStatusNode: ASTransformLayerNode { class ChatMessageDateAndStatusNode: ASTransformLayerNode {
private var backgroundNode: ASImageNode?
private var checkSentNode: ASImageNode? private var checkSentNode: ASImageNode?
private var checkReadNode: ASImageNode? private var checkReadNode: ASImageNode?
private var clockFrameNode: ASImageNode? private var clockFrameNode: ASImageNode?
@@ -106,21 +116,56 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
var clockFrameNode = self.clockFrameNode var clockFrameNode = self.clockFrameNode
var clockMinNode = self.clockMinNode var clockMinNode = self.clockMinNode
var currentBackgroundNode = self.backgroundNode
return { dateText, type, constrainedSize in return { dateText, type, constrainedSize in
let dateColor: UIColor let dateColor: UIColor
var backgroundImage: UIImage?
var outgoingStatus: ChatMessageDateAndStatusOutgoingType? var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
let leftInset: CGFloat
let loadedCheckFullImage: UIImage?
let loadedCheckPartialImage: UIImage?
let clockFrameImage: UIImage?
let clockMinImage: UIImage?
switch type { switch type {
case .BubbleIncoming: case .BubbleIncoming:
dateColor = incomingDateColor dateColor = incomingDateColor
leftInset = 10.0
loadedCheckFullImage = checkBubbleFullImage
loadedCheckPartialImage = checkBubblePartialImage
clockFrameImage = clockBubbleFrameImage
clockMinImage = clockBubbleMinImage
case let .BubbleOutgoing(status): case let .BubbleOutgoing(status):
dateColor = outgoingDateColor dateColor = outgoingDateColor
outgoingStatus = status outgoingStatus = status
leftInset = 10.0
loadedCheckFullImage = checkBubbleFullImage
loadedCheckPartialImage = checkBubblePartialImage
clockFrameImage = clockBubbleFrameImage
clockMinImage = clockBubbleMinImage
case .ImageIncoming:
dateColor = .white
backgroundImage = imageBackground
leftInset = 0.0
loadedCheckFullImage = checkMediaFullImage
loadedCheckPartialImage = checkMediaPartialImage
clockFrameImage = clockMediaFrameImage
clockMinImage = clockMediaMinImage
case let .ImageOutgoing(status):
dateColor = .white
outgoingStatus = status
backgroundImage = imageBackground
leftInset = 0.0
loadedCheckFullImage = checkMediaFullImage
loadedCheckPartialImage = checkMediaPartialImage
clockFrameImage = clockMediaFrameImage
clockMinImage = clockMediaMinImage
} }
let (date, dateApply) = dateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, nil) let (date, dateApply) = dateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, nil)
let leftInset: CGFloat = 10.0
let statusWidth: CGFloat let statusWidth: CGFloat
var checkSentFrame: CGRect? var checkSentFrame: CGRect?
@@ -128,9 +173,6 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
var clockPosition = CGPoint() var clockPosition = CGPoint()
let loadedCheckFullImage = checkFullImage
let loadedCheckPartialImage = checkPartialImage
if let outgoingStatus = outgoingStatus { if let outgoingStatus = outgoingStatus {
switch outgoingStatus { switch outgoingStatus {
case .Sending: case .Sending:
@@ -188,7 +230,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
clockFrameNode = nil clockFrameNode = nil
clockMinNode = nil clockMinNode = nil
let checkSize = checkFullImage!.size let checkSize = loadedCheckFullImage!.size
if read { if read {
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize) checkReadFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize)
@@ -211,18 +253,49 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
clockMinNode = nil clockMinNode = nil
} }
return (CGSize(width: leftInset + date.size.width + statusWidth, height: date.size.height), { [weak self] animated in var backgroundInsets = UIEdgeInsets()
if let backgroundImage = backgroundImage {
if currentBackgroundNode == nil {
let backgroundNode = ASImageNode()
backgroundNode.isLayerBacked = true
backgroundNode.displayWithoutProcessing = true
backgroundNode.displaysAsynchronously = false
backgroundNode.image = backgroundImage
currentBackgroundNode = backgroundNode
}
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
}
let layoutSize = CGSize(width: leftInset + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
return (layoutSize, { [weak self] animated in
if let strongSelf = self { if let strongSelf = self {
if backgroundImage != nil {
if let currentBackgroundNode = currentBackgroundNode {
if currentBackgroundNode.supernode == nil {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
}
strongSelf.backgroundNode?.frame = CGRect(origin: CGPoint(), size: layoutSize)
} else {
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.removeFromSupernode()
strongSelf.backgroundNode = nil
}
}
let _ = dateApply() let _ = dateApply()
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: date.size) strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top), size: date.size)
if let clockFrameNode = clockFrameNode { if let clockFrameNode = clockFrameNode {
if strongSelf.clockFrameNode == nil { if strongSelf.clockFrameNode == nil {
strongSelf.clockFrameNode = clockFrameNode strongSelf.clockFrameNode = clockFrameNode
strongSelf.addSubnode(clockFrameNode) strongSelf.addSubnode(clockFrameNode)
} }
clockFrameNode.position = clockPosition clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y)
if let clockFrameNode = strongSelf.clockFrameNode { if let clockFrameNode = strongSelf.clockFrameNode {
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
} }
@@ -236,7 +309,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
strongSelf.clockMinNode = clockMinNode strongSelf.clockMinNode = clockMinNode
strongSelf.addSubnode(clockMinNode) strongSelf.addSubnode(clockMinNode)
} }
clockMinNode.position = clockPosition clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y)
if let clockMinNode = strongSelf.clockMinNode { if let clockMinNode = strongSelf.clockMinNode {
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
} }
@@ -259,7 +332,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
animateSentNode = animated animateSentNode = animated
} }
checkSentNode.isHidden = false checkSentNode.isHidden = false
checkSentNode.frame = checkSentFrame checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top)
} else { } else {
checkSentNode.isHidden = true checkSentNode.isHidden = true
} }
@@ -277,7 +350,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
animateReadNode = animated animateReadNode = animated
} }
checkReadNode.isHidden = false checkReadNode.isHidden = false
checkReadNode.frame = checkReadFrame checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top)
} else { } else {
checkReadNode.isHidden = true checkReadNode.isHidden = true
} }

View File

@@ -18,6 +18,7 @@ struct ChatMessageItemTextLayoutConstants {
struct ChatMessageItemImageLayoutConstants { struct ChatMessageItemImageLayoutConstants {
let bubbleInsets: UIEdgeInsets let bubbleInsets: UIEdgeInsets
let statusInsets: UIEdgeInsets
let defaultCornerRadius: CGFloat let defaultCornerRadius: CGFloat
let mergedCornerRadius: CGFloat let mergedCornerRadius: CGFloat
let contentMergedCornerRadius: CGFloat let contentMergedCornerRadius: CGFloat
@@ -43,7 +44,7 @@ struct ChatMessageItemLayoutConstants {
self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFillFactor: 0.85, minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 1.0)) self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFillFactor: 0.85, minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 1.0))
self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0))
self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 260.0, height: 260.0)) self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 260.0, height: 260.0))
self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
} }
} }

View File

@@ -11,12 +11,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
} }
private let interactiveImageNode: ChatMessageInteractiveMediaNode private let interactiveImageNode: ChatMessageInteractiveMediaNode
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private var item: ChatMessageItem? private var item: ChatMessageItem?
private var media: Media? private var media: Media?
required init() { required init() {
self.interactiveImageNode = ChatMessageInteractiveMediaNode() self.interactiveImageNode = ChatMessageInteractiveMediaNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
super.init() super.init()
@@ -37,6 +39,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) {
let interactiveImageLayout = self.interactiveImageNode.asyncLayout() let interactiveImageLayout = self.interactiveImageNode.asyncLayout()
let statusLayout = self.dateAndStatusNode.asyncLayout()
return { item, layoutConstants, position, constrainedSize in return { item, layoutConstants, position, constrainedSize in
var selectedMedia: Media? var selectedMedia: Media?
@@ -58,13 +61,78 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
return (refinedWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { boundingWidth in return (refinedWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { boundingWidth in
let (imageSize, imageApply) = finishLayout(boundingWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) let (imageSize, imageApply) = finishLayout(boundingWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
return (CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom), { [weak self] _ in var t = Int(item.message.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
var edited = false
var viewCount: Int?
for attribute in item.message.attributes {
if let _ = attribute as? EditedMessageAttribute {
edited = true
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
}
}
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
if let viewCount = viewCount {
dateText = "\(viewCount) " + dateText
}
if edited {
dateText = "edited " + dateText
}
let statusType: ChatMessageDateAndStatusType?
if case .None = position.bottom {
if item.message.flags.contains(.Incoming) {
statusType = .ImageIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .ImageOutgoing(.Failed)
} else if item.message.flags.isSending {
statusType = .ImageOutgoing(.Sending)
} else {
statusType = .ImageOutgoing(.Sent(read: item.read))
}
}
} else {
statusType = nil
}
let imageLayoutSize = CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom)
var statusSize = CGSize()
var statusApply: ((Bool) -> Void)?
if let statusType = statusType {
let (size, apply) = statusLayout(dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude))
statusSize = size
statusApply = apply
}
let layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height)
return (layoutSize, { [weak self] animation in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.media = selectedMedia strongSelf.media = selectedMedia
strongSelf.interactiveImageNode.frame = CGRect(origin: CGPoint(x: layoutConstants.image.bubbleInsets.left, y: layoutConstants.image.bubbleInsets.top), size: imageSize) strongSelf.interactiveImageNode.frame = CGRect(origin: CGPoint(x: layoutConstants.image.bubbleInsets.left, y: layoutConstants.image.bubbleInsets.top), size: imageSize)
if let statusApply = statusApply {
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.interactiveImageNode.addSubnode(strongSelf.dateAndStatusNode)
}
var hasAnimation = true
if case .None = animation {
hasAnimation = false
}
statusApply(hasAnimation)
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutSize.width - layoutConstants.image.bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - layoutConstants.image.bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
} else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode()
}
imageApply() imageApply()
} }
}) })

View File

@@ -49,7 +49,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
var edited = false var edited = false
var viewCount: Int? var viewCount: Int?
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let _ = attribute as? EditedMessageAttribute {
edited = true edited = true
} else if let attribute = attribute as? ViewCountMessageAttribute { } else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count viewCount = attribute.count

View File

@@ -24,10 +24,14 @@ public class ContactSelectionController: ViewController {
return self._result.get() return self._result.get()
} }
private let createActionDisposable = MetaDisposable() private let confirmation: (PeerId) -> Signal<Bool, NoError>
public init(account: Account, title: String) { private let createActionDisposable = MetaDisposable()
private let confirmationDisposable = MetaDisposable()
public init(account: Account, title: String, confirmation: @escaping (PeerId) -> Signal<Bool, NoError> = { _ in .single(true) }) {
self.account = account self.account = account
self.confirmation = confirmation
super.init() super.init()
@@ -142,7 +146,14 @@ public class ContactSelectionController: ViewController {
} }
private func openPeer(peerId: PeerId) { private func openPeer(peerId: PeerId) {
self._result.set(.single(peerId)) self.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
self.contactsNode.animateOut() self.confirmationDisposable.set((self.confirmation(peerId) |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf._result.set(.single(peerId))
strongSelf.contactsNode.animateOut()
}
}
}))
} }
} }

View File

@@ -104,7 +104,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
}) })
case let .member(_, peer, presence): case let .member(_, peer, presence):
return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, label: nil, sectionId: self.section, action: nil) return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .activity, label: nil, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in })
} }
} }
} }

View File

@@ -0,0 +1,127 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class DebugAccountsControllerArguments {
let account: Account
let presentController: (ViewController, ViewControllerPresentationArguments) -> Void
let switchAccount: (AccountRecordId) -> Void
let loginNewAccount: () -> Void
init(account: Account, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, switchAccount: @escaping (AccountRecordId) -> Void, loginNewAccount: @escaping () -> Void) {
self.account = account
self.presentController = presentController
self.switchAccount = switchAccount
self.loginNewAccount = loginNewAccount
}
}
private enum DebugAccountsControllerSection: Int32 {
case accounts
case actions
}
private enum DebugAccountsControllerEntry: ItemListNodeEntry {
case record(AccountRecord, Bool)
case loginNewAccount
var section: ItemListSectionId {
switch self {
case .record:
return DebugAccountsControllerSection.accounts.rawValue
case .loginNewAccount:
return DebugAccountsControllerSection.actions.rawValue
}
}
var stableId: Int64 {
switch self {
case let .record(record, _):
return record.id.int64
case .loginNewAccount:
return Int64.max
}
}
static func ==(lhs: DebugAccountsControllerEntry, rhs: DebugAccountsControllerEntry) -> Bool {
switch lhs {
case let .record(record, current):
if case .record(record, current) = rhs {
return true
} else {
return false
}
case .loginNewAccount:
if case .loginNewAccount = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: DebugAccountsControllerEntry, rhs: DebugAccountsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: DebugAccountsControllerArguments) -> ListViewItem {
switch self {
case let .record(record, current):
return ItemListCheckboxItem(title: "\(UInt64(bitPattern: record.id.int64))", checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.switchAccount(record.id)
})
case .loginNewAccount:
return ItemListActionItem(title: "Login to another account", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.loginNewAccount()
})
}
}
}
private func debugAccountsControllerEntries(view: AccountRecordsView) -> [DebugAccountsControllerEntry] {
var entries: [DebugAccountsControllerEntry] = []
for entry in view.records.sorted(by: {
$0.id < $1.id
}) {
entries.append(.record(entry, entry.id == view.currentRecord?.id))
}
entries.append(.loginNewAccount)
return entries
}
public func debugAccountsController(account: Account, accountManager: AccountManager) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let arguments = DebugAccountsControllerArguments(account: account, presentController: { controller, arguments in
presentControllerImpl?(controller, arguments)
}, switchAccount: { id in
let _ = accountManager.modify({ modifier -> Void in
modifier.setCurrentId(id)
}).start()
}, loginNewAccount: {
let _ = accountManager.modify({ modifier -> Void in
let id = modifier.createRecord([])
modifier.setCurrentId(id)
}).start()
})
let signal = accountManager.accountRecords()
|> map { view -> (ItemListControllerState, (ItemListNodeState<DebugAccountsControllerEntry>, DebugAccountsControllerEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(title: "Accounts", leftNavigationButton: nil, rightNavigationButton: nil)
let listState = ItemListNodeState(entries: debugAccountsControllerEntries(view: view), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window, with: a)
}
return controller
}

View File

@@ -0,0 +1,123 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class DebugControllerArguments {
let account: Account
let accountManager: AccountManager
let presentController: (ViewController, ViewControllerPresentationArguments) -> Void
let pushController: (ViewController) -> Void
init(account: Account, accountManager: AccountManager, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, pushController: @escaping (ViewController) -> Void) {
self.account = account
self.accountManager = accountManager
self.presentController = presentController
self.pushController = pushController
}
}
private enum DebugControllerSection: Int32 {
case logs
}
private enum DebugControllerEntry: ItemListNodeEntry {
case sendLogs
case accounts
var section: ItemListSectionId {
switch self {
case .sendLogs:
return DebugControllerSection.logs.rawValue
case .accounts:
return DebugControllerSection.logs.rawValue
}
}
var stableId: Int32 {
switch self {
case .sendLogs:
return 0
case .accounts:
return 1
}
}
static func ==(lhs: DebugControllerEntry, rhs: DebugControllerEntry) -> Bool {
switch lhs {
case .sendLogs, .accounts:
return lhs.stableId == rhs.stableId
}
}
static func <(lhs: DebugControllerEntry, rhs: DebugControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: DebugControllerArguments) -> ListViewItem {
switch self {
case .sendLogs:
return ItemListDisclosureItem(title: "Seng Logs", label: "", sectionId: self.section, style: .blocks, action: {
let _ = (Logger.shared.collectLogs()
|> deliverOnMainQueue).start(next: { logs in
let controller = PeerSelectionController(account: arguments.account)
controller.peerSelected = { [weak controller] peerId in
if let strongController = controller {
strongController.dismiss()
let messages = logs.map { (name, path) -> EnqueueMessage in
let id = arc4random64()
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)])
return .message(text: "", attributes: [], media: file, replyToMessageId: nil)
}
let _ = enqueueMessages(account: arguments.account, peerId: peerId, messages: messages).start()
}
}
arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
})
})
case .accounts:
return ItemListDisclosureItem(title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: {
arguments.pushController(debugAccountsController(account: arguments.account, accountManager: arguments.accountManager))
})
}
}
}
private func debugControllerEntries() -> [DebugControllerEntry] {
var entries: [DebugControllerEntry] = []
entries.append(.sendLogs)
entries.append(.accounts)
return entries
}
public func debugController(account: Account, accountManager: AccountManager) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let arguments = DebugControllerArguments(account: account, accountManager: accountManager, presentController: { controller, arguments in
presentControllerImpl?(controller, arguments)
}, pushController: { controller in
pushControllerImpl?(controller)
})
let signal = Signal<Void, NoError>.single(Void())
|> map { _ -> (ItemListControllerState, (ItemListNodeState<DebugControllerEntry>, DebugControllerEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(title: "Debug", leftNavigationButton: nil, rightNavigationButton: nil)
let listState = ItemListNodeState(entries: debugControllerEntries(), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window, with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
return controller
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import Foundation /*import Foundation
import Postbox import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
@@ -674,3 +674,4 @@ func groupInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries
return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton)
} }
*/

View File

@@ -6,6 +6,7 @@ import SwiftSignalKit
enum ItemListActionKind { enum ItemListActionKind {
case generic case generic
case destructive case destructive
case neutral
} }
enum ItemListActionAlignment { enum ItemListActionAlignment {
@@ -110,9 +111,17 @@ class ItemListActionItemNode: ListViewItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, width, neighbors in return { item, width, neighbors in
let sectionInset: CGFloat = 22.0 let textColor: UIColor
switch item.kind {
case .destructive:
textColor = UIColor(0xff3b30)
case .generic:
textColor = UIColor(0x007ee5)
case .neutral:
textColor = .black
}
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.kind == .destructive ? UIColor(0xff3b30) : UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
let contentSize: CGSize let contentSize: CGSize
let insets: UIEdgeInsets let insets: UIEdgeInsets

View File

@@ -0,0 +1,146 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
class ItemListActivityTextItem: ListViewItem, ItemListItem {
let displayActivity: Bool
let text: NSAttributedString
let sectionId: ItemListSectionId
let isAlwaysPlain: Bool = true
init(displayActivity: Bool, text: NSAttributedString, sectionId: ItemListSectionId) {
self.displayActivity = displayActivity
self.text = text
self.sectionId = sectionId
}
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 = ItemListActivityTextItemNode()
let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
completion(node, {
return (nil, { apply() })
})
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
guard let node = node as? ItemListActivityTextItemNode else {
assertionFailure()
return
}
Queue.mainQueue().async {
let makeLayout = node.asyncLayout()
async {
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, {
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(14.0)
class ItemListActivityTextItemNode: ListViewItemNode {
private let titleNode: TextNode
private var activityIndicator: UIActivityIndicatorView?
private var item: ItemListActivityTextItem?
init() {
self.titleNode = TextNode()
self.titleNode.isLayerBacked = true
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
}
override func didLoad() {
super.didLoad()
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.activityIndicator = activityIndicator
self.view.addSubview(activityIndicator)
activityIndicator.frame = CGRect(origin: CGPoint(x: 15.0, y: 6.0), size: activityIndicator.bounds.size)
if let item = self.item {
if item.displayActivity {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
} else {
activityIndicator.isHidden = true
}
}
}
func asyncLayout() -> (_ item: ItemListActivityTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, width, neighbors in
let leftInset: CGFloat = 15.0
let verticalInset: CGFloat = 7.0
var activityWidth: CGFloat = 0.0
if item.displayActivity {
activityWidth = 25.0
}
let titleString = NSMutableAttributedString(attributedString: item.text)
titleString.removeAttribute(NSFontAttributeName, range: NSMakeRange(0, titleString.length))
titleString.addAttributes([NSFontAttributeName: titleFont], range: NSMakeRange(0, titleString.length))
let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), TextNodeCutout(position: .TopLeft, size: CGSize(width: activityWidth, height: 4.0)))
let contentSize: CGSize
let insets: UIEdgeInsets
contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset)
insets = itemListNeighborsPlainInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
if let activityIndicator = strongSelf.activityIndicator, activityIndicator.isHidden != !item.displayActivity {
if item.displayActivity {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
} else {
activityIndicator.isHidden = true
activityIndicator.stopAnimating()
}
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@@ -21,12 +21,27 @@ enum ItemListAvatarAndNameInfoItemName: Equatable {
var composedTitle: String { var composedTitle: String {
switch self { switch self {
case let .personName(firstName, lastName): case let .personName(firstName, lastName):
return firstName + " " + lastName if !firstName.isEmpty && !lastName.isEmpty {
return firstName + " " + lastName
} else if !firstName.isEmpty {
return firstName
} else {
return lastName
}
case let .title(title): case let .title(title):
return title return title
} }
} }
var isEmpty: Bool {
switch self {
case let .personName(firstName, lastName):
return !firstName.isEmpty || !lastName.isEmpty
case let .title(title):
return title.isEmpty
}
}
static func ==(lhs: ItemListAvatarAndNameInfoItemName, rhs: ItemListAvatarAndNameInfoItemName) -> Bool { static func ==(lhs: ItemListAvatarAndNameInfoItemName, rhs: ItemListAvatarAndNameInfoItemName) -> Bool {
switch lhs { switch lhs {
case let .personName(firstName, lastName): case let .personName(firstName, lastName):

View File

@@ -5,10 +5,11 @@ import SwiftSignalKit
enum ItemListNavigationButtonStyle { enum ItemListNavigationButtonStyle {
case regular case regular
case bold case bold
case activity
var barButtonItemStyle: UIBarButtonItemStyle { var barButtonItemStyle: UIBarButtonItemStyle {
switch self { switch self {
case .regular: case .regular, .activity:
return .plain return .plain
case .bold: case .bold:
return .done return .done
@@ -27,6 +28,14 @@ struct ItemListControllerState {
let title: String let title: String
let leftNavigationButton: ItemListNavigationButton? let leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton?
let animateChanges: Bool
init(title: String, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, animateChanges: Bool = true) {
self.title = title
self.leftNavigationButton = leftNavigationButton
self.rightNavigationButton = rightNavigationButton
self.animateChanges = animateChanges
}
} }
final class ItemListController<Entry: ItemListNodeEntry>: ViewController { final class ItemListController<Entry: ItemListNodeEntry>: ViewController {
@@ -73,7 +82,12 @@ final class ItemListController<Entry: ItemListNodeEntry>: ViewController {
if strongSelf.rightNavigationButtonTitleAndStyle?.0 != controllerState.rightNavigationButton?.title || strongSelf.rightNavigationButtonTitleAndStyle?.1 != controllerState.rightNavigationButton?.style { if strongSelf.rightNavigationButtonTitleAndStyle?.0 != controllerState.rightNavigationButton?.title || strongSelf.rightNavigationButtonTitleAndStyle?.1 != controllerState.rightNavigationButton?.style {
if let rightNavigationButton = controllerState.rightNavigationButton { if let rightNavigationButton = controllerState.rightNavigationButton {
let item = UIBarButtonItem(title: rightNavigationButton.title, style: rightNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed)) let item: UIBarButtonItem
if case .activity = rightNavigationButton.style {
item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode())
} else {
item = UIBarButtonItem(title: rightNavigationButton.title, style: rightNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed))
}
strongSelf.rightNavigationButtonTitleAndStyle = (rightNavigationButton.title, rightNavigationButton.style) strongSelf.rightNavigationButtonTitleAndStyle = (rightNavigationButton.title, rightNavigationButton.style)
strongSelf.navigationItem.setRightBarButton(item, animated: false) strongSelf.navigationItem.setRightBarButton(item, animated: false)
item.isEnabled = rightNavigationButton.enabled item.isEnabled = rightNavigationButton.enabled
@@ -112,6 +126,8 @@ final class ItemListController<Entry: ItemListNodeEntry>: ViewController {
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
(self.displayNode as! ItemListNode<Entry>).listNode.preloadPages = true
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments {
if case .modalSheet = presentationArguments.presentationAnimation { if case .modalSheet = presentationArguments.presentationAnimation {
(self.displayNode as! ItemListNode<Entry>).animateIn() (self.displayNode as! ItemListNode<Entry>).animateIn()
@@ -122,4 +138,16 @@ final class ItemListController<Entry: ItemListNodeEntry>: ViewController {
func dismiss() { func dismiss() {
(self.displayNode as! ItemListNode<Entry>).animateOut() (self.displayNode as! ItemListNode<Entry>).animateOut()
} }
func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? {
var result: CGRect?
(self.displayNode as! ItemListNode<Entry>).listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode {
if predicate(itemNode) {
result = itemNode.convert(itemNode.bounds, to: self.displayNode)
}
}
}
return result
}
} }

View File

@@ -39,11 +39,19 @@ private struct ItemListNodeTransition {
let updateStyle: ItemListStyle? let updateStyle: ItemListStyle?
let firstTime: Bool let firstTime: Bool
let animated: Bool let animated: Bool
let animateAlpha: Bool
} }
struct ItemListNodeState<Entry: ItemListNodeEntry> { struct ItemListNodeState<Entry: ItemListNodeEntry> {
let entries: [Entry] let entries: [Entry]
let style: ItemListStyle let style: ItemListStyle
let animateChanges: Bool
init(entries: [Entry], style: ItemListStyle, animateChanges: Bool = true) {
self.entries = entries
self.style = style
self.animateChanges = animateChanges
}
} }
final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode { final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
@@ -53,7 +61,7 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
} }
private var didSetReady = false private var didSetReady = false
private let listNode: ListView let listNode: ListView
private let transitionDisposable = MetaDisposable() private let transitionDisposable = MetaDisposable()
private var enqueuedTransitions: [ItemListNodeTransition] = [] private var enqueuedTransitions: [ItemListNodeTransition] = []
@@ -81,7 +89,7 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
if previous?.style != state.style { if previous?.style != state.style {
updatedStyle = state.style updatedStyle = state.style
} }
return ItemListNodeTransition(entries: transition, updateStyle: updatedStyle, firstTime: previous == nil, animated: previous != nil) return ItemListNodeTransition(entries: transition, updateStyle: updatedStyle, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges)
}) |> deliverOnMainQueue).start(next: { [weak self] transition in }) |> deliverOnMainQueue).start(next: { [weak self] transition in
if let strongSelf = self { if let strongSelf = self {
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
@@ -167,6 +175,9 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
options.insert(.LowLatency) options.insert(.LowLatency)
} else if transition.animated { } else if transition.animated {
options.insert(.AnimateInsertion) options.insert(.AnimateInsertion)
} else if transition.animateAlpha {
options.insert(.PreferSynchronousResourceLoading)
options.insert(.AnimateAlpha)
} }
self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self { if let strongSelf = self {

View File

@@ -123,6 +123,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode {
case .changed: case .changed:
var translation = recognizer.translation(in: self.view) var translation = recognizer.translation(in: self.view)
translation.x += self.initialRevealOffset translation.x += self.initialRevealOffset
translation.x = min(0.0, translation.x)
if self.revealNode == nil && translation.x.isLess(than: 0.0) { if self.revealNode == nil && translation.x.isLess(than: 0.0) {
self.setupAndAddRevealNode() self.setupAndAddRevealNode()
self.revealOptionsInteractivelyOpened() self.revealOptionsInteractivelyOpened()

View File

@@ -5,12 +5,14 @@ import SwiftSignalKit
class ItemListMultilineInputItem: ListViewItem, ItemListItem { class ItemListMultilineInputItem: ListViewItem, ItemListItem {
let text: String let text: String
let placeholder: String
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
let textUpdated: (String) -> Void let textUpdated: (String) -> Void
init(text: String, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { init(text: String, placeholder: String, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) {
self.text = text self.text = text
self.placeholder = placeholder
self.sectionId = sectionId self.sectionId = sectionId
self.textUpdated = textUpdated self.textUpdated = textUpdated
self.action = action self.action = action
@@ -120,6 +122,8 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: UIColor(0x878787))
return (layout, { [weak self] in return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
@@ -159,6 +163,11 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: width - leftInset, height: textLayout.size.height)) strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: width - leftInset, height: textLayout.size.height))
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - leftInset - 8.0, height: textLayout.size.height)) strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - leftInset - 8.0, height: textLayout.size.height))
} }

View File

@@ -0,0 +1,220 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
class ItemListMultilineTextItem: ListViewItem, ItemListItem {
let text: String
let sectionId: ItemListSectionId
let style: ItemListStyle
init(text: String, sectionId: ItemListSectionId, style: ItemListStyle) {
self.text = text
self.sectionId = sectionId
self.style = style
}
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 = ItemListMultilineTextItemNode()
let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
completion(node, {
return (nil, { apply() })
})
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
if let node = node as? ItemListMultilineTextItemNode {
Queue.mainQueue().async {
let makeLayout = node.asyncLayout()
async {
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, {
apply()
})
}
}
}
}
}
}
private let titleFont = Font.regular(17.0)
class ItemListMultilineTextItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let textNode: TextNode
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
self.bottomStripeNode.isLayerBacked = true
self.textNode = TextNode()
self.textNode.isLayerBacked = true
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
}
func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, width, neighbors in
let textColor: UIColor = .black
let leftInset: CGFloat
switch item.style {
case .plain:
leftInset = 35.0
case .blocks:
leftInset = 16.0
}
let (titleLayout, titleApply) = makeTextLayout(NSAttributedString(string: item.text, font: titleFont, textColor: textColor), nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
switch item.style {
case .plain:
contentSize = CGSize(width: width, height: titleLayout.size.height + 22.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
contentSize = CGSize(width: width, height: titleLayout.size.height + 22.0)
insets = itemListNeighborsGroupedInsets(neighbors)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let _ = titleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@@ -6,12 +6,14 @@ import SwiftSignalKit
class ItemListPeerActionItem: ListViewItem, ItemListItem { class ItemListPeerActionItem: ListViewItem, ItemListItem {
let icon: UIImage? let icon: UIImage?
let title: String let title: String
let editing: Bool
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
init(icon: UIImage?, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void) { init(icon: UIImage?, title: String, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) {
self.icon = icon self.icon = icon
self.title = title self.title = title
self.editing = editing
self.sectionId = sectionId self.sectionId = sectionId
self.action = action self.action = action
} }
@@ -25,7 +27,7 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem {
node.insets = layout.insets node.insets = layout.insets
completion(node, { completion(node, {
return (nil, { apply() }) return (nil, { apply(false) })
}) })
} }
} }
@@ -35,11 +37,16 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem {
Queue.mainQueue().async { Queue.mainQueue().async {
let makeLayout = node.asyncLayout() let makeLayout = node.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async { async {
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async { Queue.mainQueue().async {
completion(layout, { completion(layout, {
apply() apply(animated)
}) })
} }
} }
@@ -99,13 +106,15 @@ class ItemListPeerActionItemNode: ListViewItemNode {
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
} }
func asyncLayout() -> (_ item: ItemListPeerActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: ItemListPeerActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, width, neighbors in return { item, width, neighbors in
let leftInset: CGFloat = 65.0 let leftInset: CGFloat = 65.0
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0)
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), nil)
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
@@ -115,13 +124,20 @@ class ItemListPeerActionItemNode: ListViewItemNode {
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size let layoutSize = layout.size
return (layout, { [weak self] in return (layout, { [weak self] animated in
if let strongSelf = self { if let strongSelf = self {
let _ = titleApply() let _ = titleApply()
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.iconNode.image = item.icon strongSelf.iconNode.image = item.icon
if let image = item.icon { if let image = item.icon {
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: editingOffset + floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
} }
if strongSelf.backgroundNode.supernode == nil { if strongSelf.backgroundNode.supernode == nil {
@@ -139,11 +155,12 @@ class ItemListPeerActionItemNode: ListViewItemNode {
default: default:
strongSelf.topStripeNode.isHidden = false strongSelf.topStripeNode.isHidden = false
} }
let bottomStripeInset: CGFloat let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat let bottomStripeOffset: CGFloat
switch neighbors.bottom { switch neighbors.bottom {
case .sameSection(false): case .sameSection(false):
bottomStripeInset = leftInset bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight bottomStripeOffset = -separatorHeight
default: default:
bottomStripeInset = 0.0 bottomStripeInset = 0.0
@@ -151,9 +168,9 @@ class ItemListPeerActionItemNode: ListViewItemNode {
} }
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 12.0), size: titleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel))
} }

View File

@@ -5,21 +5,55 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
struct ItemListPeerItemEditing: Equatable {
let editable: Bool
let editing: Bool
let revealed: Bool
static func ==(lhs: ItemListPeerItemEditing, rhs: ItemListPeerItemEditing) -> Bool {
if lhs.editable != rhs.editable {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
return true
}
}
enum ItemListPeerItemText {
case activity
case text(String)
}
final class ItemListPeerItem: ListViewItem, ItemListItem { final class ItemListPeerItem: ListViewItem, ItemListItem {
let account: Account let account: Account
let peer: Peer? let peer: Peer
let presence: PeerPresence? let presence: PeerPresence?
let text: ItemListPeerItemText
let label: String? let label: String?
let editing: ItemListPeerItemEditing
let enabled: Bool
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let action: (() -> Void)? let action: (() -> Void)?
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let removePeer: (PeerId) -> Void
init(account: Account, peer: Peer?, presence: PeerPresence?, label: String?, sectionId: ItemListSectionId, action: (() -> Void)?) { init(account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: String?, editing: ItemListPeerItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) {
self.account = account self.account = account
self.peer = peer self.peer = peer
self.presence = presence self.presence = presence
self.text = text
self.label = label self.label = label
self.editing = editing
self.enabled = enabled
self.sectionId = sectionId self.sectionId = sectionId
self.action = action self.action = action
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
} }
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) { func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
@@ -31,7 +65,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
node.insets = layout.insets node.insets = layout.insets
completion(node, { completion(node, {
return (nil, { apply() }) return (nil, { apply(false) })
}) })
} }
} }
@@ -41,11 +75,16 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
Queue.mainQueue().async { Queue.mainQueue().async {
let makeLayout = node.asyncLayout() let makeLayout = node.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async { async {
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async { Queue.mainQueue().async {
completion(layout, { completion(layout, {
apply() apply(animated)
}) })
} }
} }
@@ -72,6 +111,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let titleNode: TextNode private let titleNode: TextNode
@@ -81,7 +121,12 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
private var peerPresenceManager: PeerPresenceStatusManager? private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (ItemListPeerItem, CGFloat, ItemListNeighbors)? private var layoutParams: (ItemListPeerItem, CGFloat, ItemListNeighbors)?
private var editableControlNode: ItemListEditableControlNode?
override var canBeSelected: Bool { override var canBeSelected: Bool {
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
return false
}
if let item = self.layoutParams?.0, item.action != nil { if let item = self.layoutParams?.0, item.action != nil {
return true return true
} else { } else {
@@ -134,37 +179,53 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams { if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2) let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2)
apply() apply(true)
} }
}) })
} }
func asyncLayout() -> (_ item: ItemListPeerItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: ItemListPeerItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
var currentDisabledOverlayNode = self.disabledOverlayNode
return { item, width, neighbors in return { item, width, neighbors in
var titleAttributedString: NSAttributedString? var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString?
var labelAttributedString: NSAttributedString? var labelAttributedString: NSAttributedString?
if let peer = item.peer { let peerRevealOptions: [ItemListRevealOption]
if let user = peer as? TelegramUser { if item.editing.editable && item.enabled {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { peerRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(0xff3824))]
let string = NSMutableAttributedString() } else {
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) peerRevealOptions = []
string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) }
string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black))
titleAttributedString = string if let user = item.peer as? TelegramUser {
} else if let firstName = user.firstName, !firstName.isEmpty { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) let string = NSMutableAttributedString()
} else if let lastName = user.lastName, !lastName.isEmpty { string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black))
titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black))
} else { string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black))
titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) titleAttributedString = string
} } else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black)
} else if let lastName = user.lastName, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black)
} else {
titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6))
}
} else if let group = item.peer as? TelegramGroup {
titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black)
} else if let channel = item.peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black)
}
switch item.text {
case .activity:
if let presence = item.presence as? TelegramUserPresence { if let presence = item.presence as? TelegramUserPresence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp))
@@ -172,11 +233,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
} else { } else {
statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6))
} }
} else if let group = peer as? TelegramGroup { case let .text(text):
titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6))
} else if let channel = peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black)
}
} }
if let label = item.label { if let label = item.label {
@@ -185,10 +243,21 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
let leftInset: CGFloat = 65.0 let leftInset: CGFloat = 65.0
let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)?
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width, height: CGFloat.greatestFiniteMagnitude), nil) let editingOffset: CGFloat
let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0), height: CGFloat.greatestFiniteMagnitude), nil) if item.editing.editing {
let sizeAndApply = editableControlLayout(48.0)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0.width
} else {
editingOffset = 0.0
}
let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset, height: CGFloat.greatestFiniteMagnitude), nil)
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width - editingOffset, height: CGFloat.greatestFiniteMagnitude), nil)
let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset, height: CGFloat.greatestFiniteMagnitude), nil)
let insets = itemListNeighborsGroupedInsets(neighbors) let insets = itemListNeighborsGroupedInsets(neighbors)
let contentSize = CGSize(width: width, height: 48.0) let contentSize = CGSize(width: width, height: 48.0)
@@ -197,12 +266,73 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size let layoutSize = layout.size
return (layout, { [weak self] in if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
}
} else {
currentDisabledOverlayNode = nil
}
return (layout, { [weak self] animated in
if let strongSelf = self { if let strongSelf = self {
strongSelf.layoutParams = (item, width, neighbors) strongSelf.layoutParams = (item, width, neighbors)
let revealOffset = strongSelf.revealOffset let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.addSubnode(currentDisabledOverlayNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1()
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.avatarNode)
let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
}
strongSelf.editableControlNode?.isHidden = !item.editing.editable
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
let _ = titleApply() let _ = titleApply()
let _ = statusApply() let _ = statusApply()
let _ = labelApply() let _ = labelApply()
@@ -228,30 +358,31 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
let bottomStripeOffset: CGFloat let bottomStripeOffset: CGFloat
switch neighbors.bottom { switch neighbors.bottom {
case .sameSection(false): case .sameSection(false):
bottomStripeInset = leftInset bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight bottomStripeOffset = -separatorHeight
default: default:
bottomStripeInset = 0.0 bottomStripeInset = 0.0
bottomStripeOffset = 0.0 bottomStripeOffset = 0.0
} }
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset, y: 5.0), size: titleLayout.size) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 5.0), size: titleLayout.size))
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset, y: 25.0), size: statusLayout.size) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: statusLayout.size))
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0 - labelLayout.size.height / 10.0)), size: labelLayout.size) transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0 - labelLayout.size.height / 10.0)), size: labelLayout.size))
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: revealOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)))
if let peer = item.peer { strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer)
strongSelf.avatarNode.setPeer(account: item.account, peer: peer)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel))
if let presence = item.presence as? TelegramUserPresence { if let presence = item.presence as? TelegramUserPresence {
strongSelf.peerPresenceManager?.reset(presence: presence) strongSelf.peerPresenceManager?.reset(presence: presence)
} }
strongSelf.setRevealOptions(peerRevealOptions)
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
} }
}) })
} }
@@ -306,6 +437,44 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition) super.updateRevealOffset(offset: offset, transition: transition)
let leftInset: CGFloat = 65.0
let width = self.bounds.size.width
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 5.0), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: self.statusNode.bounds.size))
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - 15.0, y: floor((self.contentSize.height - self.labelNode.bounds.size.height) / 2.0 - self.labelNode.bounds.size.height / 10.0)), size: self.labelNode.bounds.size))
transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)))
}
override func revealOptionsInteractivelyOpened() {
if let (item, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(item.peer.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let (item, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(nil, item.peer.id)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let (item, _, _) = self.layoutParams {
item.removePeer(item.peer.id)
}
} }
} }

View File

@@ -0,0 +1,191 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let title: NSAttributedString
let text: String
let placeholder: String
let sectionId: ItemListSectionId
let action: () -> Void
let textUpdated: (String) -> Void
init(title: NSAttributedString, text: String, placeholder: String, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) {
self.title = title
self.text = text
self.placeholder = placeholder
self.sectionId = sectionId
self.textUpdated = textUpdated
self.action = action
}
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 = ItemListSingleLineInputItemNode()
let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
completion(node, {
return (nil, { apply() })
})
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
if let node = node as? ItemListSingleLineInputItemNode {
Queue.mainQueue().async {
let makeLayout = node.asyncLayout()
async {
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, {
apply()
})
}
}
}
}
}
}
private let titleFont = Font.regular(17.0)
class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let titleNode: TextNode
private let textNode: TextFieldNode
private var item: ItemListSingleLineInputItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.textNode = TextFieldNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
}
override func didLoad() {
super.didLoad()
self.textNode.textField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)]
self.textNode.textField.font = Font.regular(17.0)
self.textNode.textField.textColor = .black
self.textNode.clipsToBounds = true
self.textNode.textField.delegate = self
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, width, neighbors in
let leftInset: CGFloat = 16.0
let titleString = NSMutableAttributedString(attributedString: item.title)
titleString.removeAttribute(NSFontAttributeName, range: NSMakeRange(0, titleString.length))
titleString.addAttributes([NSFontAttributeName: Font.regular(17.0)], range: NSMakeRange(0, titleString.length))
let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 32 - leftInset, height: CGFloat.greatestFiniteMagnitude), nil)
let separatorHeight = UIScreenPixel
let contentSize = CGSize(width: width, height: 44.0)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: UIColor(0x878787))
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
if let currentText = strongSelf.textNode.textField.text {
if currentText != item.text {
strongSelf.textNode.textField.text = item.text
}
} else {
strongSelf.textNode.textField.text = item.text
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - (leftInset + titleLayout.size.width)), height: 40.0))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func textFieldTextChanged(_ textField: UITextField) {
if let item = self.item {
if let text = self.textNode.textField.text {
item.textUpdated(text)
} else {
item.textUpdated("")
}
}
}
}

View File

@@ -76,13 +76,11 @@ class ItemListTextItemNode: ListViewItemNode {
let contentSize: CGSize let contentSize: CGSize
let insets: UIEdgeInsets let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset) contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset)
insets = itemListNeighborsPlainInsets(neighbors) insets = itemListNeighborsPlainInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {

View File

@@ -124,7 +124,7 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U
} }
} else { } else {
free(buffer.mData) free(buffer.mData)
trace("ManagedAudioRecorder", what: "AudioUnitRender returned \(status)") Logger.shared.log("ManagedAudioRecorder", "AudioUnitRender returned \(status)")
} }
}) })
@@ -327,17 +327,17 @@ final class ManagedAudioRecorderContext {
status = AudioOutputUnitStop(audioUnit) status = AudioOutputUnitStop(audioUnit)
if status != noErr { if status != noErr {
trace("ManagedAudioRecorder", what: "AudioOutputUnitStop returned \(status)") Logger.shared.log("ManagedAudioRecorder", "AudioOutputUnitStop returned \(status)")
} }
status = AudioUnitUninitialize(audioUnit) status = AudioUnitUninitialize(audioUnit)
if status != noErr { if status != noErr {
trace("ManagedAudioRecorder", what: "AudioUnitUninitialize returned \(status)") Logger.shared.log("ManagedAudioRecorder", "AudioUnitUninitialize returned \(status)")
} }
status = AudioComponentInstanceDispose(audioUnit) status = AudioComponentInstanceDispose(audioUnit)
if status != noErr { if status != noErr {
trace("ManagedAudioRecorder", what: "AudioComponentInstanceDispose returned \(status)") Logger.shared.log("ManagedAudioRecorder", "AudioComponentInstanceDispose returned \(status)")
} }
} }

View File

@@ -322,12 +322,12 @@ private final class AudioPlayerRendererContext {
status = AudioOutputUnitStop(audioUnit) status = AudioOutputUnitStop(audioUnit)
if status != noErr { if status != noErr {
trace("AudioPlayerRenderer", what: "AudioOutputUnitStop error \(status)") Logger.shared.log("AudioPlayerRenderer", "AudioOutputUnitStop error \(status)")
} }
status = AudioComponentInstanceDispose(audioUnit); status = AudioComponentInstanceDispose(audioUnit);
if status != noErr { if status != noErr {
trace("AudioPlayerRenderer", what: "AudioComponentInstanceDispose error \(status)") Logger.shared.log("AudioPlayerRenderer", "AudioComponentInstanceDispose error \(status)")
} }
self.audioUnit = nil self.audioUnit = nil
} }

View File

@@ -263,7 +263,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in
if let value = value { if let value = value {
arguments?.updateMessageSound(value) arguments?.updateGroupSound(value)
} }
})) }))
}) })

View File

@@ -4,6 +4,21 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramCore import TelegramCore
func peerInfoController(account: Account, peer: Peer) -> ViewController? {
if let _ = peer as? TelegramGroup {
return groupInfoController(account: account, peerId: peer.id)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return groupInfoController(account: account, peerId: peer.id)
}
}
return nil
}
final class PeerInfoControllerInteraction { final class PeerInfoControllerInteraction {
let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void
let openSharedMedia: () -> Void let openSharedMedia: () -> Void

View File

@@ -48,7 +48,7 @@ struct PeerInfoEntries {
} }
func peerInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { func peerInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries {
if let user = view.peers[view.peerId] as? TelegramUser { /*if let user = view.peers[view.peerId] as? TelegramUser {
return userInfoEntries(view: view, state: state) return userInfoEntries(view: view, state: state)
} else if let secretChat = view.peers[view.peerId] as? TelegramSecretChat { } else if let secretChat = view.peers[view.peerId] as? TelegramSecretChat {
return userInfoEntries(view: view, state: state) return userInfoEntries(view: view, state: state)
@@ -61,6 +61,6 @@ func peerInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries {
} }
} else if let group = view.peers[view.peerId] as? TelegramGroup { } else if let group = view.peers[view.peerId] as? TelegramGroup {
return groupInfoEntries(view: view, state: state) return groupInfoEntries(view: view, state: state)
} }*/
return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil) return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil)
} }

View File

@@ -1,75 +1,405 @@
import Foundation import Foundation
import Display import Display
import Postbox
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
public class SettingsController: ListController { private struct SettingsItemArguments {
private let account: Account let account: Account
let accountManager: AccountManager
private let peer = Promise<Peer>() let pushController: (ViewController) -> Void
private let connectionStatus = Promise<ConnectionStatus>(.Online) let presentController: (ViewController) -> Void
private let peerAndConnectionStatusDisposable = MetaDisposable() let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let saveEditingState: () -> Void
public init(account: Account) { let logout: () -> Void
self.account = account }
super.init()
self.title = "Settings"
self.tabBarItem.title = "Settings"
self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")?.precomposed()
self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected")?.precomposed()
let deselectAction = { [weak self] () -> Void in
self?.listDisplayNode.listView.clearHighlightAnimated(true)
}
self.items = [
SettingsAccountInfoItem(account: account, peer: nil, connectionStatus: .Online),
ListControllerButtonItem(title: "Set Profile Photo", action: deselectAction),
ListControllerSpacerItem(height: 35.0),
ListControllerDisclosureActionItem(title: "Notifications and Sounds", action: deselectAction),
ListControllerDisclosureActionItem(title: "Privacy and Security", action: deselectAction),
ListControllerDisclosureActionItem(title: "Data and Storage", action: { [weak self] in
deselectAction()
if let strongSelf = self {
strongSelf.navigationController?.pushViewController(DataAndStorageSettingsController(account: strongSelf.account), animated: true)
}
}),
//SettingsWallpaperListItem(),
ListControllerSpacerItem(height: 35.0),
ListControllerDisclosureActionItem(title: "Phone Number", action: deselectAction),
ListControllerDisclosureActionItem(title: "Username", action: deselectAction),
ListControllerSpacerItem(height: 35.0),
ListControllerDisclosureActionItem(title: "Ask a Question", action: deselectAction),
ListControllerDisclosureActionItem(title: "Telegram FAQ", action: deselectAction),
ListControllerSpacerItem(height: 35.0),
ListControllerButtonItem(title: "Logout", action: { }, color: UIColor.red),
ListControllerSpacerItem(height: 35.0)
]
let peerAndConnectionStatus = combineLatest(peer.get(), connectionStatus.get()) |> deliverOn(Queue.mainQueue()) |> afterNext { [weak self] peer, connectionStatus in
if let strongSelf = self {
let item = SettingsAccountInfoItem(account: account, peer: peer, connectionStatus: connectionStatus)
strongSelf.items[0] = item
if strongSelf.isNodeLoaded {
strongSelf.listDisplayNode.listView.transaction(deleteIndices: [ListViewDeleteItem(index: 0, directionHint: nil)], insertIndicesAndItems: [ListViewInsertItem(index: 0, previousIndex: 0, item: item, directionHint: .Down)], updateIndicesAndItems: [], options: [.AnimateInsertion], updateOpaqueState: nil)
}
}
}
peerAndConnectionStatusDisposable.set(peerAndConnectionStatus.start())
peer.set(account.postbox.loadedPeerWithId(account.peerId))
connectionStatus.set(account.network.connectionStatus)
}
required public init(coder aDecoder: NSCoder) { private enum SettingsSection: Int32 {
fatalError("init(coder:) has not been implemented") case info
case generalSettings
case accountSettings
case help
case logOut
}
private enum SettingsEntry: ItemListNodeEntry {
case userInfo(Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState)
case setProfilePhoto
case notificationsAndSounds
case privacyAndSecurity
case dataAndStorage
case stickers
case phoneNumber(String)
case username(String)
case askAQuestion
case faq
case debug
case logOut
var section: ItemListSectionId {
switch self {
case .userInfo, .setProfilePhoto:
return SettingsSection.info.rawValue
case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .stickers:
return SettingsSection.generalSettings.rawValue
case .phoneNumber, .username:
return SettingsSection.accountSettings.rawValue
case .askAQuestion, .faq, .debug:
return SettingsSection.help.rawValue
case .logOut:
return SettingsSection.logOut.rawValue
}
} }
deinit { var stableId: Int32 {
peerAndConnectionStatusDisposable.dispose() switch self {
case .userInfo:
return 0
case .setProfilePhoto:
return 1
case .notificationsAndSounds:
return 2
case .privacyAndSecurity:
return 3
case .dataAndStorage:
return 4
case .stickers:
return 5
case .phoneNumber:
return 6
case .username:
return 7
case .askAQuestion:
return 8
case .faq:
return 9
case .debug:
return 10
case .logOut:
return 11
}
}
static func ==(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool {
switch lhs {
case let .userInfo(lhsPeer, lhsCachedData, lhsEditingState):
if case let .userInfo(rhsPeer, rhsCachedData, rhsEditingState) = rhs {
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer != nil) != (rhsPeer != nil) {
return false
}
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
if !lhsCachedData.isEqual(to: rhsCachedData) {
return false
}
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
return false
}
if lhsEditingState != rhsEditingState {
return false
}
return true
} else {
return false
}
case .setProfilePhoto:
if case .setProfilePhoto = rhs {
return true
} else {
return false
}
case .notificationsAndSounds:
if case .notificationsAndSounds = rhs {
return true
} else {
return false
}
case .privacyAndSecurity:
if case .privacyAndSecurity = rhs {
return true
} else {
return false
}
case .dataAndStorage:
if case .dataAndStorage = rhs {
return true
} else {
return false
}
case .stickers:
if case .stickers = rhs {
return true
} else {
return false
}
case let .phoneNumber(number):
if case .phoneNumber(number) = rhs {
return true
} else {
return false
}
case let .username(address):
if case .username(address) = rhs {
return true
} else {
return false
}
case .askAQuestion:
if case .askAQuestion = rhs {
return true
} else {
return false
}
case .faq:
if case .faq = rhs {
return true
} else {
return false
}
case .debug:
if case .debug = rhs {
return true
} else {
return false
}
case .logOut:
if case .logOut = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: SettingsItemArguments) -> ListViewItem {
switch self {
case let .userInfo(peer, cachedData, state):
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 and Sounds", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.pushController(notificationsAndSoundsController(account: arguments.account))
})
case .privacyAndSecurity:
return ItemListDisclosureItem(title: "Privacy and Security", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .dataAndStorage:
return ItemListDisclosureItem(title: "Data and Storage", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .stickers:
return ItemListDisclosureItem(title: "Stickers", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case let .phoneNumber(number):
return ItemListDisclosureItem(title: "Phone Number", label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case let .username(address):
return ItemListDisclosureItem(title: "Username", label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .askAQuestion:
return ItemListDisclosureItem(title: "Ask a Question", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .faq:
return ItemListDisclosureItem(title: "Telegram FAQ", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .debug:
return ItemListDisclosureItem(title: "Debug", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.pushController(debugController(account: arguments.account, accountManager: arguments.accountManager))
})
case .logOut:
return ItemListActionItem(title: "Log Out", kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.logout()
})
}
} }
} }
private struct SettingsEditingState: Equatable {
let editingName: ItemListAvatarAndNameInfoItemName
static func ==(lhs: SettingsEditingState, rhs: SettingsEditingState) -> Bool {
if lhs.editingName != rhs.editingName {
return false
}
return true
}
}
private struct SettingsState: Equatable {
let editingState: SettingsEditingState?
let updatingName: ItemListAvatarAndNameInfoItemName?
func withUpdatedEditingState(_ editingState: SettingsEditingState?) -> SettingsState {
return SettingsState(editingState: editingState, updatingName: self.updatingName)
}
func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> SettingsState {
return SettingsState(editingState: self.editingState, updatingName: updatingName)
}
static func ==(lhs: SettingsState, rhs: SettingsState) -> Bool {
if lhs.editingState != rhs.editingState {
return false
}
if lhs.updatingName != rhs.updatingName {
return false
}
return true
}
}
private func settingsEntries(state: SettingsState, view: PeerView) -> [SettingsEntry] {
var entries: [SettingsEntry] = []
if let peer = peerViewMainPeer(view) as? TelegramUser {
let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName)
entries.append(.userInfo(peer, view.cachedData, userInfoState))
entries.append(.setProfilePhoto)
entries.append(.notificationsAndSounds)
entries.append(.privacyAndSecurity)
entries.append(.dataAndStorage)
entries.append(.stickers)
if let phone = peer.phone {
entries.append(.phoneNumber(formatPhoneNumber(phone)))
}
entries.append(.username(peer.addressName == nil ? "" : ("@" + peer.addressName!)))
entries.append(.askAQuestion)
entries.append(.faq)
entries.append(.debug)
if let _ = state.editingState {
entries.append(.logOut)
}
}
return entries
}
public func settingsController(account: Account, accountManager: AccountManager) -> ViewController {
let statePromise = ValuePromise(SettingsState(editingState: nil, updatingName: nil), ignoreRepeated: true)
let stateValue = Atomic(value: SettingsState(editingState: nil, updatingName: nil))
let updateState: ((SettingsState) -> SettingsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let updatePeerNameDisposable = MetaDisposable()
actionsDisposable.add(updatePeerNameDisposable)
let arguments = SettingsItemArguments(account: account, accountManager: accountManager, pushController: { controller in
pushControllerImpl?(controller)
}, presentController: { controller in
presentControllerImpl?(controller)
}, updateEditingName: { editingName in
updateState { state in
if let _ = state.editingState {
return state.withUpdatedEditingState(SettingsEditingState(editingName: editingName))
} else {
return state
}
}
}, saveEditingState: {
var updateName: ItemListAvatarAndNameInfoItemName?
updateState { state in
if let editingState = state.editingState {
updateName = editingState.editingName
return state.withUpdatedEditingState(nil).withUpdatedUpdatingName(editingState.editingName)
} else {
return state
}
}
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updatePeerNameDisposable.set((updateAccountPeerName(account: account, firstName: firstName, lastName: lastName) |> afterDisposed {
Queue.mainQueue().async {
updateState { state in
return state.withUpdatedUpdatingName(nil)
}
}
}).start())
}
}, logout: {
let alertController = standardTextAlertController(title: NSLocalizedString("Settings.LogoutConfirmationTitle", comment: ""), text: NSLocalizedString("Settings.LogoutConfirmationText", comment: ""), actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .defaultAction, title: "OK", action: {
let _ = logoutFromAccount(id: account.id, accountManager: accountManager).start()
})
])
presentControllerImpl?(alertController)
})
let peerView = account.viewTracker.peerView(account.peerId)
let signal = combineLatest(statePromise.get(), peerView)
|> map { state, view -> (ItemListControllerState, (ItemListNodeState<SettingsEntry>, SettingsEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
let rightNavigationButton: ItemListNavigationButton
if let _ = state.editingState {
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
arguments.saveEditingState()
})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
if let peer = peer as? TelegramUser {
updateState { state in
return state.withUpdatedEditingState(SettingsEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName)))
}
}
})
}
let controllerState = ItemListControllerState(title: "Settings", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton)
let listState = ItemListNodeState(entries: settingsEntries(state: state, view: view), style: .blocks)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
controller.tabBarItem.title = "Settings"
controller.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")?.precomposed()
controller.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected")?.precomposed()
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

@@ -1,380 +0,0 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private struct SettingsItemArguments {
let account: Account
let pushController: (ViewController) -> Void
let presentController: (ViewController) -> Void
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let saveEditingState: () -> Void
}
private enum SettingsSection: Int32 {
case info
case generalSettings
case accountSettings
case help
case logOut
}
private enum SettingsEntry: ItemListNodeEntry {
case userInfo(Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState)
case setProfilePhoto
case notificationsAndSounds
case privacyAndSecurity
case dataAndStorage
case stickers
case phoneNumber(String)
case username(String)
case askAQuestion
case faq
case logOut
var section: ItemListSectionId {
switch self {
case .userInfo, .setProfilePhoto:
return SettingsSection.info.rawValue
case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .stickers:
return SettingsSection.generalSettings.rawValue
case .phoneNumber, .username:
return SettingsSection.accountSettings.rawValue
case .askAQuestion, .faq:
return SettingsSection.help.rawValue
case .logOut:
return SettingsSection.logOut.rawValue
}
}
var stableId: Int32 {
switch self {
case .userInfo:
return 0
case .setProfilePhoto:
return 1
case .notificationsAndSounds:
return 2
case .privacyAndSecurity:
return 3
case .dataAndStorage:
return 4
case .stickers:
return 5
case .phoneNumber:
return 6
case .username:
return 7
case .askAQuestion:
return 8
case .faq:
return 9
case .logOut:
return 10
}
}
static func ==(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool {
switch lhs {
case let .userInfo(lhsPeer, lhsCachedData, lhsEditingState):
if case let .userInfo(rhsPeer, rhsCachedData, rhsEditingState) = rhs {
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer != nil) != (rhsPeer != nil) {
return false
}
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
if !lhsCachedData.isEqual(to: rhsCachedData) {
return false
}
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
return false
}
if lhsEditingState != rhsEditingState {
return false
}
return true
} else {
return false
}
case .setProfilePhoto:
if case .setProfilePhoto = rhs {
return true
} else {
return false
}
case .notificationsAndSounds:
if case .notificationsAndSounds = rhs {
return true
} else {
return false
}
case .privacyAndSecurity:
if case .privacyAndSecurity = rhs {
return true
} else {
return false
}
case .dataAndStorage:
if case .dataAndStorage = rhs {
return true
} else {
return false
}
case .stickers:
if case .stickers = rhs {
return true
} else {
return false
}
case let .phoneNumber(number):
if case .phoneNumber(number) = rhs {
return true
} else {
return false
}
case let .username(address):
if case .username(address) = rhs {
return true
} else {
return false
}
case .askAQuestion:
if case .askAQuestion = rhs {
return true
} else {
return false
}
case .faq:
if case .faq = rhs {
return true
} else {
return false
}
case .logOut:
if case .logOut = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: SettingsItemArguments) -> ListViewItem {
switch self {
case let .userInfo(peer, cachedData, state):
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 and Sounds", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.pushController(notificationsAndSoundsController(account: arguments.account))
})
case .privacyAndSecurity:
return ItemListDisclosureItem(title: "Privacy and Security", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .dataAndStorage:
return ItemListDisclosureItem(title: "Data and Storage", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .stickers:
return ItemListDisclosureItem(title: "Stickers", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case let .phoneNumber(number):
return ItemListDisclosureItem(title: "Phone Number", label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case let .username(address):
return ItemListDisclosureItem(title: "Username", label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .askAQuestion:
return ItemListDisclosureItem(title: "Ask a Question", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .faq:
return ItemListDisclosureItem(title: "Telegram FAQ", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
case .logOut:
return ItemListActionItem(title: "Log Out", kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
})
}
}
}
private struct SettingsEditingState: Equatable {
let editingName: ItemListAvatarAndNameInfoItemName
static func ==(lhs: SettingsEditingState, rhs: SettingsEditingState) -> Bool {
if lhs.editingName != rhs.editingName {
return false
}
return true
}
}
private struct SettingsState: Equatable {
let editingState: SettingsEditingState?
let updatingName: ItemListAvatarAndNameInfoItemName?
func withUpdatedEditingState(_ editingState: SettingsEditingState?) -> SettingsState {
return SettingsState(editingState: editingState, updatingName: self.updatingName)
}
func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> SettingsState {
return SettingsState(editingState: self.editingState, updatingName: updatingName)
}
static func ==(lhs: SettingsState, rhs: SettingsState) -> Bool {
if lhs.editingState != rhs.editingState {
return false
}
if lhs.updatingName != rhs.updatingName {
return false
}
return true
}
}
private func settingsEntries(state: SettingsState, view: PeerView) -> [SettingsEntry] {
var entries: [SettingsEntry] = []
if let peer = peerViewMainPeer(view) as? TelegramUser {
let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName)
entries.append(.userInfo(peer, view.cachedData, userInfoState))
entries.append(.setProfilePhoto)
entries.append(.notificationsAndSounds)
entries.append(.privacyAndSecurity)
entries.append(.dataAndStorage)
entries.append(.stickers)
if let phone = peer.phone {
entries.append(.phoneNumber(formatPhoneNumber(phone)))
}
entries.append(.username(peer.addressName == nil ? "" : ("@" + peer.addressName!)))
entries.append(.askAQuestion)
entries.append(.faq)
if let _ = state.editingState {
entries.append(.logOut)
}
}
return entries
}
public func settingsController(account: Account) -> ViewController {
let statePromise = ValuePromise(SettingsState(editingState: nil, updatingName: nil), ignoreRepeated: true)
let stateValue = Atomic(value: SettingsState(editingState: nil, updatingName: nil))
let updateState: ((SettingsState) -> SettingsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let updatePeerNameDisposable = MetaDisposable()
actionsDisposable.add(updatePeerNameDisposable)
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 {
return state.withUpdatedEditingState(SettingsEditingState(editingName: editingName))
} else {
return state
}
}
}, saveEditingState: {
var updateName: ItemListAvatarAndNameInfoItemName?
updateState { state in
if let editingState = state.editingState {
updateName = editingState.editingName
return state.withUpdatedEditingState(nil).withUpdatedUpdatingName(editingState.editingName)
} else {
return state
}
}
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updatePeerNameDisposable.set((updateAccountPeerName(account: account, firstName: firstName, lastName: lastName) |> afterDisposed {
Queue.mainQueue().async {
updateState { state in
return state.withUpdatedUpdatingName(nil)
}
}
}).start())
}
})
let peerView = account.viewTracker.peerView(account.peerId)
let signal = combineLatest(statePromise.get(), peerView)
|> map { state, view -> (ItemListControllerState, (ItemListNodeState<SettingsEntry>, SettingsEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
let rightNavigationButton: ItemListNavigationButton
if let _ = state.editingState {
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
arguments.saveEditingState()
})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .bold, enabled: true, action: {
if let peer = peer as? TelegramUser {
updateState { state in
return state.withUpdatedEditingState(SettingsEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName)))
}
}
})
}
let controllerState = ItemListControllerState(title: "Settings", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton)
let listState = ItemListNodeState(entries: settingsEntries(state: state, view: view), style: .blocks)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
controller.tabBarItem.title = "Settings"
controller.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")?.precomposed()
controller.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected")?.precomposed()
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

@@ -10,9 +10,12 @@ public final class TelegramApplicationContext {
let sharedChatMediaInputNode = Atomic<ChatMediaInputNode?>(value: nil) let sharedChatMediaInputNode = Atomic<ChatMediaInputNode?>(value: nil)
let mediaManager = MediaManager() let mediaManager = MediaManager()
public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void) { public let applicationInForeground: Signal<Bool, NoError>
public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal<Bool, NoError>) {
self.openUrl = openUrl self.openUrl = openUrl
self.getTopWindow = getTopWindow self.getTopWindow = getTopWindow
self.displayNotification = displayNotification self.displayNotification = displayNotification
self.applicationInForeground = applicationInForeground
} }
} }

View File

@@ -184,7 +184,7 @@ final class TextNode: ASDisplayNode {
var lineAdditionalWidth: CGFloat = 0.0 var lineAdditionalWidth: CGFloat = 0.0
if cutoutEnabled { if cutoutEnabled {
if lineOriginY < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY {
lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth)
lineCutoutOffset = cutoutOffset lineCutoutOffset = cutoutOffset
lineAdditionalWidth = cutoutWidth lineAdditionalWidth = cutoutWidth