diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 34106a0aac..096e4f1745 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -38,7 +38,7 @@ D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; }; D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.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 */; }; D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.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 */; }; D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.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 */; }; D049EAE41E44949F00A2CD3A /* HorizontalStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.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 */; }; D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.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 */; }; D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.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 */; }; D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.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 */; }; D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.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 */; }; D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.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 */; }; D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; }; 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 */; }; D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.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 */; }; D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.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 */; }; D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.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 */; }; D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E801D6B8C850046BCD6 /* FastBlur.m */; }; 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 = ""; }; D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerNode.swift; sourceTree = ""; }; D01B27981E39144C0022A4C0 /* ItemListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListController.swift; sourceTree = ""; }; - D01B279A1E39386C0022A4C0 /* SettingsControllerEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsControllerEntries.swift; sourceTree = ""; }; + D01B279A1E39386C0022A4C0 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsAndSounds.swift; sourceTree = ""; }; D01B279E1E394BD70022A4C0 /* InAppNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppNotificationSettings.swift; sourceTree = ""; }; D01B27A31E394FC90022A4C0 /* SecuritySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuritySettings.swift; sourceTree = ""; }; @@ -551,6 +552,9 @@ D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = ""; }; D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = ""; }; D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; + D03E5E081E55C49C0029569A /* DebugAccountsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugAccountsController.swift; sourceTree = ""; }; + D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelVisibilityController.swift; sourceTree = ""; }; + D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = ""; }; D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = ""; }; D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickerGridItem.swift; sourceTree = ""; }; D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputRecentStickerPacksItem.swift; sourceTree = ""; }; @@ -629,6 +633,10 @@ D050F2121E48B61500988324 /* PhoneInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneInputNode.swift; sourceTree = ""; }; D050F2151E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionController.swift; sourceTree = ""; }; D050F2171E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceCountrySelectionControllerNode.swift; sourceTree = ""; }; + D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSingleLineInputItem.swift; sourceTree = ""; }; + D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActivityTextItem.swift; sourceTree = ""; }; + D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListMultilineTextItem.swift; sourceTree = ""; }; + D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminsController.swift; sourceTree = ""; }; D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; @@ -705,6 +713,7 @@ D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = ""; }; D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = ""; }; D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceStatusManager.swift; sourceTree = ""; }; + D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlacklistController.swift; sourceTree = ""; }; D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = ""; }; @@ -715,6 +724,7 @@ D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatButtonKeyboardInputNode.swift; sourceTree = ""; }; D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBotInfoItem.swift; sourceTree = ""; }; D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = ""; }; + D0CE1BD21E51BC6100404327 /* DebugController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugController.swift; sourceTree = ""; }; D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = ""; }; D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; @@ -820,13 +830,6 @@ D0F69DDC1D6B8A420046BCD6 /* ListControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerItem.swift; sourceTree = ""; }; D0F69DDD1D6B8A420046BCD6 /* ListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerNode.swift; sourceTree = ""; }; D0F69DDE1D6B8A420046BCD6 /* ListControllerSpacerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListControllerSpacerItem.swift; sourceTree = ""; }; - D0F69DE81D6B8A6C0046BCD6 /* AuthorizationCodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeController.swift; sourceTree = ""; }; - D0F69DE91D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeControllerNode.swift; sourceTree = ""; }; - D0F69DEA1D6B8A6C0046BCD6 /* AuthorizationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationController.swift; sourceTree = ""; }; - D0F69DEB1D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordController.swift; sourceTree = ""; }; - D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = ""; }; - D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = ""; }; - D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = ""; }; D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarNode.swift; sourceTree = ""; }; D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = ""; }; D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = ""; }; @@ -886,7 +889,6 @@ D0F69E711D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSectionHeaderAccessoryItem.swift; sourceTree = ""; }; D0F69E721D6B8C340046BCD6 /* ContactsVCardItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsVCardItem.swift; sourceTree = ""; }; D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAccountInfoItem.swift; sourceTree = ""; }; - D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; D0F69E7F1D6B8C850046BCD6 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = ""; }; D0F69E801D6B8C850046BCD6 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = ""; }; D0F69E811D6B8C850046BCD6 /* FFMpegSwResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSwResample.h; sourceTree = ""; }; @@ -956,13 +958,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - D003702C1DA43006004308D3 /* Components */ = { - isa = PBXGroup; - children = ( - ); - name = Components; - sourceTree = ""; - }; D00C7CDA1E3776CA0080C3D5 /* Secret Preview */ = { isa = PBXGroup; children = ( @@ -1479,11 +1474,12 @@ D0C932391E0B4AC60074F044 /* Settings */ = { isa = PBXGroup; children = ( - D01B279A1E39386C0022A4C0 /* SettingsControllerEntries.swift */, + D01B279A1E39386C0022A4C0 /* SettingsController.swift */, D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, - D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */, D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */, + D0CE1BD21E51BC6100404327 /* DebugController.swift */, + D03E5E081E55C49C0029569A /* DebugAccountsController.swift */, ); name = Settings; sourceTree = ""; @@ -1686,9 +1682,12 @@ D00C7CD61E3664070080C3D5 /* ItemListMultilineInputItem.swift */, D00B3F9D1E3A4847003872C3 /* ItemListSectionHeaderItem.swift */, D00B3FA11E3A983E003872C3 /* ItemListTextItem.swift */, + D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */, D021E0A81E3AACA200AF709C /* ItemListEditableItem.swift */, D021E0AA1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift */, D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, + D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */, + D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, ); name = Items; sourceTree = ""; @@ -1711,7 +1710,6 @@ D0EE97131D88BB1A006C18E1 /* Peer Info */ = { isa = PBXGroup; children = ( - D003702C1DA43006004308D3 /* Components */, D0EE97141D88BB39006C18E1 /* Controller */, ); name = "Peer Info"; @@ -1725,6 +1723,10 @@ D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */, D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, + D0486F091E523C8500091F0C /* GroupInfoController.swift */, + D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */, + D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */, + D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */, ); name = Controller; sourceTree = ""; @@ -1884,13 +1886,6 @@ D04BB2B71E44E5CB00650E93 /* Phone Entry */, D04BB2BC1E44FD1300650E93 /* Code 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; sourceTree = ""; @@ -2395,8 +2390,10 @@ D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */, D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */, + D03E5E0F1E55F8B90029569A /* ChannelVisibilityController.swift in Sources */, D0D03B1E1DECB0FE00220C46 /* opusfile.c in Sources */, D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */, + D0561DE61E57424700E6B9E9 /* ItemListMultilineTextItem.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, @@ -2417,7 +2414,6 @@ D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */, D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */, D0DE77001D92F1EB002B8809 /* ChatTitleView.swift in Sources */, - D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */, D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */, D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */, D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, @@ -2483,6 +2479,7 @@ D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */, D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */, D0215D561E043020001A0B1E /* InstantPageControllerNode.swift in Sources */, + D0B98E7F1E575D2C008084B1 /* ChannelBlacklistController.swift in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, D0D03B121DECB0FE00220C46 /* bitwise.c in Sources */, @@ -2566,6 +2563,7 @@ D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */, D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, + D03E5E091E55C49C0029569A /* DebugAccountsController.swift in Sources */, D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */, @@ -2591,15 +2589,14 @@ D0F917B51E0DA396003687E6 /* GenerateTextEntities.swift in Sources */, D0736F2E1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift in Sources */, D04BB33E1E48797500650E93 /* platform_log.c in Sources */, - D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */, - D01B279B1E39386C0022A4C0 /* SettingsControllerEntries.swift in Sources */, + D01B279B1E39386C0022A4C0 /* SettingsController.swift in Sources */, D08775201E3F595000A97350 /* ContactListActionItem.swift in Sources */, D0F69E751D6B8C340046BCD6 /* ContactsPeerItem.swift in Sources */, D01B27991E39144C0022A4C0 /* ItemListController.swift in Sources */, + D0561DE11E57153000E6B9E9 /* ItemListActivityTextItem.swift in Sources */, D0F69DD61D6B8A2D0046BCD6 /* AlertController.swift in Sources */, D00370301DA43077004308D3 /* ItemListItem.swift in Sources */, D0215D381E040F53001A0B1E /* InstantPageNode.swift in Sources */, - D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, @@ -2611,12 +2608,10 @@ D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */, D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */, D04BB36F1E48797500650E93 /* RMGeometry.m in Sources */, - D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */, D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */, D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */, D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */, - D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */, D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */, D023837E1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift in Sources */, D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */, @@ -2625,7 +2620,7 @@ D04BB3381E48797500650E93 /* rngs.c in Sources */, D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */, - D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */, + D0486F0A1E523C8500091F0C /* GroupInfoController.swift in Sources */, D04BB33C1E48797500650E93 /* timing.c in Sources */, D01B27A41E394FC90022A4C0 /* SecuritySettings.swift in Sources */, D050F2181E48D9EA00988324 /* AuthorizationSequenceCountrySelectionControllerNode.swift in Sources */, @@ -2643,12 +2638,14 @@ D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */, D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */, + D0561DDF1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift in Sources */, D01749551E1082770057C89A /* StoredMessageFromSearchPeer.swift in Sources */, D04BB32C1E48797500650E93 /* animations.c in Sources */, D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */, D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */, D0215D581E04302E001A0B1E /* InstantPageTileNode.swift in Sources */, D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */, + D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, @@ -2667,11 +2664,9 @@ D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */, D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */, - D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */, D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */, D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */, - D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */, D017494E1E1059570057C89A /* StringWithAppliedEntities.swift in Sources */, D0DE77231D932043002B8809 /* PeerMediaCollectionInterfaceState.swift in Sources */, D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */, @@ -2679,6 +2674,7 @@ D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */, D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */, D03120F61DA534C1006A2A60 /* ItemListActionItem.swift in Sources */, + D0CE1BD31E51BC6100404327 /* DebugController.swift in Sources */, D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */, D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */, D0DC35441DE32230000195EB /* ChatInterfaceStateContextQueries.swift in Sources */, diff --git a/TelegramUI/AuthorizationCodeController.swift b/TelegramUI/AuthorizationCodeController.swift deleted file mode 100644 index 339a679317..0000000000 --- a/TelegramUI/AuthorizationCodeController.swift +++ /dev/null @@ -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() - var result: Signal { - 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 - } - } -} diff --git a/TelegramUI/AuthorizationCodeControllerNode.swift b/TelegramUI/AuthorizationCodeControllerNode.swift deleted file mode 100644 index 46b5343006..0000000000 --- a/TelegramUI/AuthorizationCodeControllerNode.swift +++ /dev/null @@ -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)) - } -} diff --git a/TelegramUI/AuthorizationController.swift b/TelegramUI/AuthorizationController.swift deleted file mode 100644 index 7166faadc0..0000000000 --- a/TelegramUI/AuthorizationController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Foundation -import Display -import SwiftSignalKit -import TelegramCore - -public class AuthorizationController: NavigationController { - private var account: UnauthorizedAccount! - - private let authorizedAccountValue = Promise() - public var authorizedAccount: Signal { - 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 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 { - } -} diff --git a/TelegramUI/AuthorizationPasswordController.swift b/TelegramUI/AuthorizationPasswordController.swift deleted file mode 100644 index 0cdce1c786..0000000000 --- a/TelegramUI/AuthorizationPasswordController.swift +++ /dev/null @@ -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() - var result: Signal { - 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) - } - })) - } -} diff --git a/TelegramUI/AuthorizationPasswordControllerNode.swift b/TelegramUI/AuthorizationPasswordControllerNode.swift deleted file mode 100644 index 6b4ddc10d5..0000000000 --- a/TelegramUI/AuthorizationPasswordControllerNode.swift +++ /dev/null @@ -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)) - } -} diff --git a/TelegramUI/AuthorizationPhoneController.swift b/TelegramUI/AuthorizationPhoneController.swift deleted file mode 100644 index a4a788fa4f..0000000000 --- a/TelegramUI/AuthorizationPhoneController.swift +++ /dev/null @@ -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)) - } - })) - } -} diff --git a/TelegramUI/AuthorizationPhoneControllerNode.swift b/TelegramUI/AuthorizationPhoneControllerNode.swift deleted file mode 100644 index 3945d8111d..0000000000 --- a/TelegramUI/AuthorizationPhoneControllerNode.swift +++ /dev/null @@ -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)) - } -} diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 7f47589503..f5ecc87eb7 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -77,60 +77,26 @@ public final class AuthorizationSequenceController: NavigationController { controller.loginWithNumber = { [weak self, weak controller] number in if let strongSelf = self { controller?.inProgress = true - let account = strongSelf.account - let sendCode = Api.functions.auth.sendCode(flags: 0, phoneNumber: number, currentNumber: nil, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb") - - let signal = account.network.request(sendCode, automaticFloodWait: false) - |> map { result in - return (result, account) - } |> `catch` { error -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in - switch error.errorDescription { - case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"): - let range = error.errorDescription.range(of: "MIGRATE_")! - let updatedMasterDatacenterId = Int32(error.errorDescription.substring(from: range.upperBound))! - let updatedAccount = account.changedMasterDatacenterId(updatedMasterDatacenterId) - return updatedAccount - |> mapToSignalPromotingError { updatedAccount -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in - return updatedAccount.network.request(sendCode, automaticFloodWait: false) - |> map { sentCode in - return (sentCode, updatedAccount) - } - } - case _: - return .fail(error) - } - } - - strongSelf.actionDisposable.set(signal.start(next: { [weak self] (result, account) in + strongSelf.actionDisposable.set((sendAuthorizationCode(account: strongSelf.account, phoneNumber: number, apiId: 10840, apiHash: "33c45224029d59cb3ad0c16134215aeb") |> deliverOnMainQueue).start(next: { [weak self] account in if let strongSelf = self { + controller?.inProgress = false strongSelf.account = account - let masterDatacenterId = account.masterDatacenterId - let _ = (strongSelf.account.postbox.modify { modifier -> Void in - switch result { - case let .sentCode(_, type, phoneCodeHash, nextType, timeout): - var parsedNextType: AuthorizationCodeNextType? - if let nextType = nextType { - parsedNextType = AuthorizationCodeNextType(apiType: nextType) - } - modifier.setState(UnauthorizedAccountState(masterDatacenterId: masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType))) - } - }).start() } }, error: { error in - Queue.mainQueue().async { - if let controller = controller { - controller.inProgress = false - - var text: String? - if error.errorDescription.hasPrefix("FLOOD_WAIT") { + if let controller = controller { + controller.inProgress = false + + let text: String + switch error { + case .limitExceeded: 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." - } - if let text = text { - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) - } + case .generic: + text = "An error occurred. Please try again later." } + + 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 { controller?.inProgress = true - let account = strongSelf.account - let masterDatacenterId = account.masterDatacenterId - let signal = account.postbox.modify { modifier -> Signal 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 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 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 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 in - return .complete() - } - } - default: - break - } - } - return .complete() - } - |> mapError { _ -> String in - return "" - } - |> switchToLatest - strongSelf.actionDisposable.set(signal.start(error: { error in + strongSelf.actionDisposable.set((authorizeWithCode(account: strongSelf.account, code: code) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let controller = controller { controller.inProgress = false - var text: String? - if error.hasPrefix("FLOOD_WAIT") { - text = "You have entered invalid code too many times. Please try again later." - } else if error == "CODE_INVALID" { - text = "Invalid code." - } else { - text = "An error occured."; - } - if let text = text { - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + let text: String + switch error { + case .limitExceeded: + text = "You have entered invalid code too many times. Please try again later." + case .invalidCode: + text = "Invalid code. Please try again." + case .generic: + text = "An error occured." } + + 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 { controller?.inProgress = true - let account = strongSelf.account - let signal = verifyPassword(account, password: password) - |> `catch` { error -> Signal in - return .fail(error.errorDescription) - } - |> mapToSignal { result -> Signal 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 in - return .complete() - } - } - strongSelf.actionDisposable.set(signal.start(error: { error in + strongSelf.actionDisposable.set((authorizeWithPassword(account: strongSelf.account, password: password) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let controller = controller { controller.inProgress = false - var text: String? - if error.hasPrefix("FLOOD_WAIT") { - text = "You have entered invalid password too many times. Please try again later." - } else if error == "PASSWORD_HASH_INVALID" { - text = "Invalid password." - } else { - text = "An error occured."; - } - if let text = text { - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + let text: String + switch error { + case .limitExceeded: + text = "You have entered invalid password too many times. Please try again later." + case .invalidPassword: + text = "Invalid password. Please try again." + case .generic: + text = "An error occured. Please try again later." } + + 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) } } 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 in + self._authorizedAccount.set(accountWithId(self.account.id, appGroupPath: self.account.appGroupPath, testingEnvironment: self.account.testingEnvironment) |> mapToSignal { account -> Signal in if case let .right(authorizedAccount) = account { return .single(authorizedAccount) } else { diff --git a/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/TelegramUI/AuthorizationSequencePasswordEntryController.swift index 0d1b1d1a53..9eeb25a646 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -42,6 +42,10 @@ final class AuthorizationSequencePasswordEntryController: ViewController { self.displayNode = AuthorizationSequencePasswordEntryControllerNode() self.displayNodeDidLoad() + self.controllerNode.loginWithCode = { [weak self] _ in + self?.nextPressed() + } + if let hint = self.hint { self.controllerNode.updateData(hint: hint) } diff --git a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift index 55d468de6f..e987c389fb 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift @@ -2,7 +2,7 @@ import Foundation import AsyncDisplayKit import Display -final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode { +final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UITextFieldDelegate { private let navigationBackgroundNode: ASDisplayNode private let stripeNode: ASDisplayNode private let titleNode: ASTextNode @@ -67,6 +67,8 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode { self.backgroundColor = UIColor.white + self.codeField.textField.delegate = self + self.addSubnode(self.navigationBackgroundNode) self.addSubnode(self.stripeNode) self.addSubnode(self.codeSeparatorNode) @@ -171,4 +173,9 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode { @objc func passwordFieldTextChanged(_ textField: UITextField) { } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.loginWithCode?(self.currentPassword) + return false + } } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index 5d12d90109..7a3d6bbc59 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -84,8 +84,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } @objc func nextPressed() { - let (code, number) = self.controllerNode.codeAndNumber - if code != nil && !number.isEmpty { + let (_, number) = self.controllerNode.codeAndNumber + if !number.isEmpty { self.loginWithNumber?(self.controllerNode.currentNumber) } else { hapticFeedback.error() diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift new file mode 100644 index 0000000000..09ca114d47 --- /dev/null +++ b/TelegramUI/ChannelAdminsController.swift @@ -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.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 +} diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift new file mode 100644 index 0000000000..fbf287572c --- /dev/null +++ b/TelegramUI/ChannelBlacklistController.swift @@ -0,0 +1,2 @@ +import Foundation + diff --git a/TelegramUI/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift index 4adae27ac0..36d4762e6e 100644 --- a/TelegramUI/ChannelInfoEntries.swift +++ b/TelegramUI/ChannelInfoEntries.swift @@ -1,4 +1,4 @@ -import Foundation +/*import Foundation import Postbox import TelegramCore import SwiftSignalKit @@ -234,4 +234,4 @@ func channelBroadcastInfoEntries(view: PeerView) -> PeerInfoEntries { } } return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil) -} +}*/ diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift new file mode 100644 index 0000000000..d25a1ddcdb --- /dev/null +++ b/TelegramUI/ChannelVisibilityController.swift @@ -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.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 +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index cb1084fdd4..a835cce4b1 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -133,6 +133,7 @@ public class ChatController: TelegramController { } })) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { var transitionNode: ASDisplayNode? @@ -1029,7 +1030,7 @@ public class ChatController: TelegramController { super.viewDidAppear(animated) 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() @@ -1041,11 +1042,11 @@ public class ChatController: TelegramController { override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.chatDisplayNode.historyNode.canReadHistory.set(false) + self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) let peerId = self.peerId let timestamp = Int32(Date().timeIntervalSince1970) 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 return interfaceState }) @@ -1213,9 +1214,10 @@ public class ChatController: TelegramController { self.navigationActionDisposable.set((self.peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let _ = peerView.peers[peerView.peerId] { - let chatInfoController = PeerInfoController(account: strongSelf.account, peerId: peerView.peerId) - (strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController) + if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + } } })) break diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index aab58d16eb..db614a6fe6 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -193,7 +193,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) - let canReadHistory = ValuePromise() + let canReadHistory = Promise() private let _chatHistoryLocation = ValuePromise() private var chatHistoryLocation: Signal { diff --git a/TelegramUI/ChatHistoryNode.swift b/TelegramUI/ChatHistoryNode.swift index fa5340570b..702798965e 100644 --- a/TelegramUI/ChatHistoryNode.swift +++ b/TelegramUI/ChatHistoryNode.swift @@ -31,5 +31,5 @@ public protocol ChatHistoryNode: class { func messageInCurrentHistoryView(_ id: MessageId) -> Message? func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) - func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) + func forEachItemNode(_ f: (ASDisplayNode) -> Void) } diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 929aa75cfa..e0441b7984 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -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) var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index b315781695..9108d75b83 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -6,7 +6,7 @@ import SwiftSignalKit 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 context.translateBy(x: size.width / 2.0, y: size.height / 2.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.scaleBy(x: 0.5, y: 0.5) - context.setStrokeColor(UIColor(0x19C700).cgColor) + context.setStrokeColor(color.cgColor) context.setLineWidth(2.5) if partial { 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 context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x42b649).cgColor) - context.setFillColor(UIColor(0x42b649).cgColor) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) let strokeWidth: CGFloat = 1.0 context.setLineWidth(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 context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x42b649).cgColor) + context.setFillColor(color.cgColor) 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)) }) @@ -61,14 +61,21 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { layer.add(basicAnimation, forKey: "clockFrameAnimation") } -private let checkFullImage = generateCheckImage(partial: false) -private let checkPartialImage = generateCheckImage(partial: true) +private let checkBubbleFullImage = generateCheckImage(partial: false, color: UIColor(0x19C700)) +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 outgoingDateColor = UIColor(0x008c09, 0.8) -private let clockFrameImage = generateClockFrameImage() -private let clockMinImage = generateClockMinImage() +private let imageBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(white: 0.0, alpha: 0.5)) + +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 { case Sent(read: Bool) @@ -79,9 +86,12 @@ enum ChatMessageDateAndStatusOutgoingType { enum ChatMessageDateAndStatusType { case BubbleIncoming case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) + case ImageIncoming + case ImageOutgoing(ChatMessageDateAndStatusOutgoingType) } class ChatMessageDateAndStatusNode: ASTransformLayerNode { + private var backgroundNode: ASImageNode? private var checkSentNode: ASImageNode? private var checkReadNode: ASImageNode? private var clockFrameNode: ASImageNode? @@ -106,21 +116,56 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { var clockFrameNode = self.clockFrameNode var clockMinNode = self.clockMinNode + var currentBackgroundNode = self.backgroundNode + return { dateText, type, constrainedSize in let dateColor: UIColor + var backgroundImage: UIImage? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? + let leftInset: CGFloat + + let loadedCheckFullImage: UIImage? + let loadedCheckPartialImage: UIImage? + let clockFrameImage: UIImage? + let clockMinImage: UIImage? + switch type { case .BubbleIncoming: dateColor = incomingDateColor + leftInset = 10.0 + loadedCheckFullImage = checkBubbleFullImage + loadedCheckPartialImage = checkBubblePartialImage + clockFrameImage = clockBubbleFrameImage + clockMinImage = clockBubbleMinImage case let .BubbleOutgoing(status): dateColor = outgoingDateColor 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 leftInset: CGFloat = 10.0 - let statusWidth: CGFloat var checkSentFrame: CGRect? @@ -128,9 +173,6 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { var clockPosition = CGPoint() - let loadedCheckFullImage = checkFullImage - let loadedCheckPartialImage = checkPartialImage - if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: @@ -188,7 +230,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { clockFrameNode = nil clockMinNode = nil - let checkSize = checkFullImage!.size + let checkSize = loadedCheckFullImage!.size if read { 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 } - 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 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() - 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 strongSelf.clockFrameNode == nil { strongSelf.clockFrameNode = 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 { maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) } @@ -236,7 +309,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { strongSelf.clockMinNode = 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 { maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) } @@ -259,7 +332,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { animateSentNode = animated } checkSentNode.isHidden = false - checkSentNode.frame = checkSentFrame + checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top) } else { checkSentNode.isHidden = true } @@ -277,7 +350,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { animateReadNode = animated } checkReadNode.isHidden = false - checkReadNode.frame = checkReadFrame + checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top) } else { checkReadNode.isHidden = true } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 6e6d3a1300..e992e5e916 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -18,6 +18,7 @@ struct ChatMessageItemTextLayoutConstants { struct ChatMessageItemImageLayoutConstants { let bubbleInsets: UIEdgeInsets + let statusInsets: UIEdgeInsets let defaultCornerRadius: CGFloat let mergedCornerRadius: 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.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)) } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 71f02adee7..f1dc0028c0 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -11,12 +11,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } private let interactiveImageNode: ChatMessageInteractiveMediaNode + private let dateAndStatusNode: ChatMessageDateAndStatusNode private var item: ChatMessageItem? private var media: Media? required init() { self.interactiveImageNode = ChatMessageInteractiveMediaNode() + self.dateAndStatusNode = ChatMessageDateAndStatusNode() 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))) { let interactiveImageLayout = self.interactiveImageNode.asyncLayout() + let statusLayout = self.dateAndStatusNode.asyncLayout() return { item, layoutConstants, position, constrainedSize in var selectedMedia: Media? @@ -58,13 +61,78 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { 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) - 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 { strongSelf.item = item strongSelf.media = selectedMedia 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() } }) diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index fb70bfdceb..cbb4912767 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -49,7 +49,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var edited = false var viewCount: Int? for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { + if let _ = attribute as? EditedMessageAttribute { edited = true } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index 830d96fa13..2e41120826 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -24,10 +24,14 @@ public class ContactSelectionController: ViewController { return self._result.get() } - private let createActionDisposable = MetaDisposable() + private let confirmation: (PeerId) -> Signal - public init(account: Account, title: String) { + private let createActionDisposable = MetaDisposable() + private let confirmationDisposable = MetaDisposable() + + public init(account: Account, title: String, confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { self.account = account + self.confirmation = confirmation super.init() @@ -142,7 +146,14 @@ public class ContactSelectionController: ViewController { } private func openPeer(peerId: PeerId) { - self._result.set(.single(peerId)) - self.contactsNode.animateOut() + self.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) + 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() + } + } + })) } } diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index 17ee1d0cf1..b917d46b80 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -104,7 +104,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { }) 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 }) } } } diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift new file mode 100644 index 0000000000..d12443a430 --- /dev/null +++ b/TelegramUI/DebugAccountsController.swift @@ -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.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 +} diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift new file mode 100644 index 0000000000..2df44c31ce --- /dev/null +++ b/TelegramUI/DebugController.swift @@ -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.single(Void()) + |> map { _ -> (ItemListControllerState, (ItemListNodeState, 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 +} diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift new file mode 100644 index 0000000000..95fab34a70 --- /dev/null +++ b/TelegramUI/GroupInfoController.swift @@ -0,0 +1,1120 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() + +private final class GroupInfoArguments { + let account: Account + let peerId: PeerId + + let pushController: (ViewController) -> Void + let presentController: (ViewController, ViewControllerPresentationArguments) -> Void + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updateEditingDescriptionText: (String) -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let addMember: () -> Void + let removePeer: (PeerId) -> Void + + init(account: Account, peerId: PeerId, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void) { + self.account = account + self.peerId = peerId + self.pushController = pushController + self.presentController = presentController + self.updateEditingName = updateEditingName + self.updateEditingDescriptionText = updateEditingDescriptionText + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.addMember = addMember + self.removePeer = removePeer + } +} + +private enum GroupInfoSection: ItemListSectionId { + case info + case about + case sharedMediaAndNotifications + case infoManagement + case memberManagement + case members + case leave +} + +private enum GroupInfoMemberStatus { + case member + case admin +} + +private enum GroupEntryStableId: Hashable, Equatable { + case peer(PeerId) + case index(Int) + + static func ==(lhs: GroupEntryStableId, rhs: GroupEntryStableId) -> Bool { + switch lhs { + case let .peer(peerId): + if case .peer(peerId) = rhs { + return true + } else { + return false + } + case let .index(index): + if case .index(index) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .peer(peerId): + return peerId.hashValue + case let .index(index): + return index.hashValue + } + } +} + +private enum GroupInfoEntry: ItemListNodeEntry { + case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) + case setGroupPhoto + case about(String) + case link(String) + case sharedMedia + case notifications(settings: PeerNotificationSettings?) + case groupTypeSetup(isPublic: Bool) + case groupDescriptionSetup(text: String) + case groupManagementInfoLabel(text: String) + case membersAdmins(count: Int) + case membersBlacklist(count: Int) + case addMember(editing: Bool) + case member(index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) + case leave + + var section: ItemListSectionId { + switch self { + case .info, .setGroupPhoto: + return GroupInfoSection.info.rawValue + case .about, .link: + return GroupInfoSection.about.rawValue + case .sharedMedia, .notifications: + return GroupInfoSection.sharedMediaAndNotifications.rawValue + case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel: + return GroupInfoSection.infoManagement.rawValue + case .membersAdmins, .membersBlacklist: + return GroupInfoSection.memberManagement.rawValue + case .addMember, .member: + return GroupInfoSection.members.rawValue + case .leave: + return GroupInfoSection.leave.rawValue + } + } + + static func ==(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> Bool { + switch lhs { + case let .info(lhsPeer, lhsCachedData, lhsState): + if case let .info(rhsPeer, rhsCachedData, rhsState) = 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 lhsState != rhsState { + return false + } + return true + } else { + return false + } + case .setGroupPhoto, .sharedMedia, .leave: + return lhs.sortIndex == rhs.sortIndex + case let .about(text): + if case .about(text) = rhs { + return true + } else { + return false + } + case let .link(text): + if case .link(text) = rhs { + return true + } else { + return false + } + case let .notifications(lhsSettings): + if case let .notifications(rhsSettings) = rhs { + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + } else { + return false + } + case let .groupTypeSetup(isPublic): + if case .groupTypeSetup(isPublic) = rhs { + return true + } else { + return false + } + case let .groupDescriptionSetup(text): + if case .groupDescriptionSetup(text) = rhs { + return true + } else { + return false + } + case let .groupManagementInfoLabel(text): + if case .groupManagementInfoLabel(text) = rhs { + return true + } else { + return false + } + case let .membersAdmins(lhsCount): + if case let .membersAdmins(rhsCount) = rhs, lhsCount == rhsCount { + return true + } else { + return false + } + case let .membersBlacklist(lhsCount): + if case let .membersBlacklist(rhsCount) = rhs, lhsCount == rhsCount { + return true + } else { + return false + } + case let .addMember(editing): + if case .addMember(editing) = rhs { + return true + } else { + return false + } + case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): + if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsMemberStatus != rhsMemberStatus { + return false + } + if lhsPeerId != rhsPeerId { + return false + } + if !lhsPeer.isEqual(rhsPeer) { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhsPresence != nil) != (rhsPresence != nil) { + return false + } + if lhsEditing != rhsEditing { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + } + } + + var stableId: GroupEntryStableId { + switch self { + case let .member(_, peerId, _, _, _, _, _): + return .peer(peerId) + default: + return .index(self.sortIndex) + } + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case .setGroupPhoto: + return 1 + case .about: + return 2 + case .link: + return 3 + case .notifications: + return 4 + case .sharedMedia: + return 5 + case .groupTypeSetup: + return 6 + case .groupDescriptionSetup: + return 7 + case .groupManagementInfoLabel: + return 8 + case .membersAdmins: + return 9 + case .membersBlacklist: + return 10 + case .addMember: + return 11 + case let .member(index, _, _, _, _, _, _): + return 20 + index + case .leave: + return 1000000 + } + } + + static func <(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(_ arguments: GroupInfoArguments) -> ListViewItem { + switch self { + case let .info(peer, cachedData, state): + return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }) + case .setGroupPhoto: + return ItemListActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + }) + case let .about(text): + return ItemListMultilineTextItem(text: text, sectionId: self.section, style: .blocks) + case let .link(url): + return ItemListActionItem(title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { + }) + case let .notifications(settings): + let label: String + if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { + label = "Disabled" + } else { + label = "Enabled" + } + return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { + //interaction.changeNotificationMuteSettings() + }) + case .sharedMedia: + return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { + //interaction.openSharedMedia() + }) + case let .addMember(editing): + return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section, editing: editing, action: { + arguments.addMember() + }) + case let .groupTypeSetup(isPublic): + return ItemListDisclosureItem(title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: { + arguments.presentController(channelVisibilityController(account: arguments.account, peerId: arguments.peerId), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) + }) + case let .groupDescriptionSetup(text): + return ItemListMultilineInputItem(text: text, placeholder: "Group Description", sectionId: self.section, textUpdated: { updatedText in + arguments.updateEditingDescriptionText(updatedText) + }, action: { + + }) + case let .membersAdmins(count): + return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: { + arguments.pushController(ChannelAdminsController(account: arguments.account, peerId: arguments.peerId)) + }) + case let .membersBlacklist(count): + return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: { + + }) + case let .member(_, _, peer, presence, memberStatus, editing, enabled): + let label: String? + switch memberStatus { + case .admin: + label = "admin" + case .member: + label = nil + } + return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .activity, label: label, editing: editing, enabled: enabled, sectionId: self.section, action: { + if let infoController = peerInfoController(account: arguments.account, peer: peer) { + arguments.pushController(infoController) + } + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + arguments.setPeerIdWithRevealedOptions(peerId, fromPeerId) + }, removePeer: { peerId in + arguments.removePeer(peerId) + }) + case .leave: + return ItemListActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + }) + default: + preconditionFailure() + } + } +} + +private struct TemporaryParticipant: Equatable { + let peer: Peer + let presence: PeerPresence? + let timestamp: Int32 + + static func ==(lhs: TemporaryParticipant, rhs: TemporaryParticipant) -> Bool { + if !lhs.peer.isEqual(rhs.peer) { + return false + } + if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhs.presence != nil) != (rhs.presence != nil) { + return false + } + return true + } +} + +private struct GroupInfoState: Equatable { + let editingState: GroupInfoEditingState? + let updatingName: ItemListAvatarAndNameInfoItemName? + let peerIdWithRevealedOptions: PeerId? + + let temporaryParticipants: [TemporaryParticipant] + let successfullyAddedParticipantIds: Set + let removingParticipantIds: Set + + let savingData: Bool + + static func ==(lhs: GroupInfoState, rhs: GroupInfoState) -> Bool { + if lhs.editingState != rhs.editingState { + return false + } + if lhs.updatingName != rhs.updatingName { + return false + } + if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { + return false + } + if lhs.temporaryParticipants != rhs.temporaryParticipants { + return false + } + if lhs.successfullyAddedParticipantIds != rhs.successfullyAddedParticipantIds { + return false + } + if lhs.removingParticipantIds != rhs.removingParticipantIds { + return false + } + if lhs.savingData != rhs.savingData { + return false + } + return true + } + + func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState { + return GroupInfoState(editingState: editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedTemporaryParticipants(_ temporaryParticipants: [TemporaryParticipant]) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedSuccessfullyAddedParticipantIds(_ successfullyAddedParticipantIds: Set) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedRemovingParticipantIds(_ removingParticipantIds: Set) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData) + } + + func withUpdatedSavingData(_ savingData: Bool) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData) + } +} + +private struct GroupInfoEditingState: Equatable { + let editingName: ItemListAvatarAndNameInfoItemName? + let editingDescriptionText: String + + func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> GroupInfoEditingState { + return GroupInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText) + } + + static func ==(lhs: GroupInfoEditingState, rhs: GroupInfoEditingState) -> Bool { + if lhs.editingName != rhs.editingName { + return false + } + if lhs.editingDescriptionText != rhs.editingDescriptionText { + return false + } + return true + } +} + +private func canRemoveParticipant(account: Account, isAdmin: Bool, participantId: PeerId, invitedBy: PeerId?) -> Bool { + if participantId == account.peerId { + return false + } + + if account.peerId == invitedBy { + return true + } + + return isAdmin +} + +private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfoState) -> [GroupInfoEntry] { + var entries: [GroupInfoEntry] = [] + if let peer = peerViewMainPeer(view) { + let infoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName) + entries.append(.info(peer: peer, cachedData: view.cachedData, state: infoState)) + } + + var highlightAdmins = false + var canManageGroup = false + var canManageMembers = false + var isPublic = false + if let group = view.peers[view.peerId] as? TelegramGroup { + if group.flags.contains(.adminsEnabled) { + highlightAdmins = true + switch group.role { + case .admin, .creator: + canManageGroup = true + canManageMembers = true + case .member: + break + } + } else { + canManageGroup = true + switch group.role { + case .admin, .creator: + canManageMembers = true + case .member: + break + } + } + } else if let channel = view.peers[view.peerId] as? TelegramChannel { + highlightAdmins = true + isPublic = channel.username != nil + switch channel.role { + case .creator: + canManageGroup = true + canManageMembers = true + case .moderator: + canManageMembers = true + case .editor, .member: + break + } + } + + if canManageGroup { + entries.append(GroupInfoEntry.setGroupPhoto) + } + + if let editingState = state.editingState { + if let cachedChannelData = view.cachedData as? CachedChannelData { + entries.append(GroupInfoEntry.groupTypeSetup(isPublic: isPublic)) + entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText)) + + if let adminCount = cachedChannelData.participantsSummary.adminCount { + entries.append(GroupInfoEntry.membersAdmins(count: adminCount)) + } + if let bannedCount = cachedChannelData.participantsSummary.bannedCount { + entries.append(GroupInfoEntry.membersBlacklist(count: bannedCount)) + } + } + } else { + if let cachedChannelData = view.cachedData as? CachedChannelData { + if let about = cachedChannelData.about, !about.isEmpty { + entries.append(.about(about)) + } + if let peer = view.peers[view.peerId] as? TelegramChannel, let username = peer.username, !username.isEmpty { + entries.append(.link("t.me/" + username)) + } + } + + entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings)) + entries.append(GroupInfoEntry.sharedMedia) + } + + var canRemoveAnyMember = false + if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + for participant in participants.participants { + if canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: participant.peerId, invitedBy: participant.invitedBy) { + canRemoveAnyMember = true + break + } + } + } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { + for participant in participants.participants { + if canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: participant.peerId, invitedBy: nil) { + canRemoveAnyMember = true + break + } + } + } + + if canManageGroup { + entries.append(GroupInfoEntry.addMember(editing: state.editingState != nil && canRemoveAnyMember)) + } + + if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + var updatedParticipants = participants.participants + let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) + + var peerPresences: [PeerId: PeerPresence] = view.peerPresences + var peers: [PeerId: Peer] = view.peers + var disabledPeerIds = state.removingParticipantIds + + if !state.temporaryParticipants.isEmpty { + for participant in state.temporaryParticipants { + if !existingParticipantIds.contains(participant.peer.id) { + updatedParticipants.append(.member(id: participant.peer.id, invitedBy: account.peerId, invitedAt: participant.timestamp)) + if let presence = participant.presence, peerPresences[participant.peer.id] == nil { + peerPresences[participant.peer.id] = presence + } + if peers[participant.peer.id] == nil { + peers[participant.peer.id] = participant.peer + } + disabledPeerIds.insert(participant.peer.id) + } + } + } + + let sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in + let lhsPresence = peerPresences[lhs.peerId] as? TelegramUserPresence + let rhsPresence = peerPresences[rhs.peerId] as? TelegramUserPresence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + + switch lhs { + case .creator: + return false + case let .admin(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .admin(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + case let .member(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .admin(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + } + }) + + for i in 0 ..< sortedParticipants.count { + if let peer = peers[sortedParticipants[i].peerId] { + let memberStatus: GroupInfoMemberStatus + if highlightAdmins { + switch sortedParticipants[i] { + case .admin, .creator: + memberStatus = .admin + case .member: + memberStatus = .member + } + } else { + memberStatus = .member + } + entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + } + } + } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { + var updatedParticipants = participants.participants + let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) + var peerPresences: [PeerId: PeerPresence] = view.peerPresences + var peers: [PeerId: Peer] = view.peers + var disabledPeerIds = state.removingParticipantIds + + if !state.temporaryParticipants.isEmpty { + for participant in state.temporaryParticipants { + if !existingParticipantIds.contains(participant.peer.id) { + updatedParticipants.append(.member(id: participant.peer.id, invitedAt: participant.timestamp)) + if let presence = participant.presence, peerPresences[participant.peer.id] == nil { + peerPresences[participant.peer.id] = presence + } + if peers[participant.peer.id] == nil { + peers[participant.peer.id] = participant.peer + } + disabledPeerIds.insert(participant.peer.id) + } + } + } + + let sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in + let lhsPresence = peerPresences[lhs.peerId] as? TelegramUserPresence + let rhsPresence = peerPresences[rhs.peerId] as? TelegramUserPresence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + + switch lhs { + case .creator: + return false + case let .moderator(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .moderator(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .editor(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + case let .editor(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .moderator(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .editor(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + case let .member(lhsId, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .moderator(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .editor(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + } + }) + + for i in 0 ..< sortedParticipants.count { + if let peer = peers[sortedParticipants[i].peerId] { + let memberStatus: GroupInfoMemberStatus + if highlightAdmins { + switch sortedParticipants[i] { + case .moderator, .editor, .creator: + memberStatus = .admin + case .member: + memberStatus = .member + } + } else { + memberStatus = .member + } + entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + } + } + } + + if let group = view.peers[view.peerId] as? TelegramGroup { + if case .Member = group.membership { + entries.append(GroupInfoEntry.leave) + } + } else if let channel = view.peers[view.peerId] as? TelegramChannel { + if case .member = channel.participationStatus { + entries.append(GroupInfoEntry.leave) + } + } + + return entries +} + +private func valuesRequiringUpdate(state: GroupInfoState, view: PeerView) -> (title: String?, description: String?) { + if let peer = view.peers[view.peerId] as? TelegramGroup { + if let editingState = state.editingState { + if let title = editingState.editingName?.composedTitle, title != peer.title { + return (title, nil) + } + } + return (nil, nil) + } else if let peer = view.peers[view.peerId] as? TelegramChannel { + var titleValue: String? + var descriptionValue: String? + if let editingState = state.editingState { + if let title = editingState.editingName?.composedTitle, title != peer.title { + titleValue = title + } + if let cachedData = view.cachedData as? CachedChannelData { + if let about = cachedData.about { + if about != editingState.editingDescriptionText { + descriptionValue = editingState.editingDescriptionText + } + } else if !editingState.editingDescriptionText.isEmpty { + descriptionValue = editingState.editingDescriptionText + } + } + } + + return (titleValue, descriptionValue) + } else { + return (nil, nil) + } +} + +public func groupInfoController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(GroupInfoState(editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false), ignoreRepeated: true) + let stateValue = Atomic(value: GroupInfoState(editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false)) + let updateState: ((GroupInfoState) -> GroupInfoState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? + + let actionsDisposable = DisposableSet() + + if peerId.namespace == Namespaces.Peer.CloudChannel { + actionsDisposable.add(account.viewTracker.updatedCachedChannelParticipants(peerId, forceImmediateUpdate: true).start()) + } + + let updatePeerNameDisposable = MetaDisposable() + actionsDisposable.add(updatePeerNameDisposable) + + let updatePeerDescriptionDisposable = MetaDisposable() + actionsDisposable.add(updatePeerDescriptionDisposable) + + let addMemberDisposable = MetaDisposable() + actionsDisposable.add(addMemberDisposable) + + let removeMemberDisposable = MetaDisposable() + actionsDisposable.add(removeMemberDisposable) + + let arguments = GroupInfoArguments(account: account, peerId: peerId, pushController: { controller in + pushControllerImpl?(controller) + }, presentController: { controller, presentationArguments in + presentControllerImpl?(controller, presentationArguments) + }, updateEditingName: { editingName in + updateState { state in + if let editingState = state.editingState { + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: editingName, editingDescriptionText: editingState.editingDescriptionText)) + } else { + return state + } + } + }, updateEditingDescriptionText: { text in + updateState { state in + if let editingState = state.editingState { + return state.withUpdatedEditingState(editingState.withUpdatedEditingDescriptionText(text)) + } + return state + } + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + updateState { state in + if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { + return state.withUpdatedPeerIdWithRevealedOptions(peerId) + } else { + return state + } + } + }, addMember: { + var confirmationImpl: ((PeerId) -> Signal)? + let contactsController = ContactSelectionController(account: account, title: "Add Member", confirmation: { peerId in + if let confirmationImpl = confirmationImpl { + return confirmationImpl(peerId) + } else { + return .single(false) + } + }) + confirmationImpl = { [weak contactsController] peerId in + return account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue + |> mapToSignal { peer in + let result = ValuePromise() + if let contactsController = contactsController { + let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle)?", actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + result.set(false) + }), + TextAlertAction(type: .defaultAction, title: "OK", action: { + result.set(true) + }) + ]) + contactsController.present(alertController, in: .window) + } + + return result.get() + } + } + let addMember = contactsController.result + |> deliverOnMainQueue + |> mapToSignal { memberId -> Signal in + if let memberId = memberId { + return account.postbox.peerView(id: memberId) + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { view -> Signal in + if let peer = view.peers[memberId] { + updateState { state in + var found = false + for participant in state.temporaryParticipants { + if participant.peer.id == memberId { + found = true + break + } + } + if !found { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var temporaryParticipants = state.temporaryParticipants + temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.peerPresences[memberId], timestamp: timestamp)) + return state.withUpdatedTemporaryParticipants(temporaryParticipants) + } else { + return state + } + } + } + + return addPeerMember(account: account, peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterCompleted { + updateState { state in + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.insert(memberId) + + return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + } |> `catch` { _ -> Signal in + updateState { state in + var temporaryParticipants = state.temporaryParticipants + for i in 0 ..< temporaryParticipants.count { + if temporaryParticipants[i].peer.id == memberId { + temporaryParticipants.remove(at: i) + break + } + } + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.remove(memberId) + + return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + + return .complete() + } + } + } else { + return .complete() + } + } + presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + addMemberDisposable.set(addMember.start()) + }, removePeer: { memberId in + let signal = account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue + |> mapToSignal { peer -> Signal in + let result = ValuePromise() + + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Remove \(peer.displayTitle)?", color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + result.set(true) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + + return result.get() + } + |> mapToSignal { value -> Signal in + if value { + updateState { state in + var temporaryParticipants = state.temporaryParticipants + for i in 0 ..< state.temporaryParticipants.count { + if state.temporaryParticipants[i].peer.id == memberId { + temporaryParticipants.remove(at: i) + break + } + } + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.remove(memberId) + + var removingParticipantIds = state.removingParticipantIds + removingParticipantIds.insert(memberId) + + return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds).withUpdatedRemovingParticipantIds(removingParticipantIds) + } + + return removePeerMember(account: account, peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterDisposed { + updateState { state in + var removingParticipantIds = state.removingParticipantIds + removingParticipantIds.remove(memberId) + + return state.withUpdatedRemovingParticipantIds(removingParticipantIds) + } + } + } else { + return .complete() + } + } + removeMemberDisposable.set(signal.start()) + }) + + let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) + |> map { state, view -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in + let peer = peerViewMainPeer(view) + let rightNavigationButton: ItemListNavigationButton + if let editingState = state.editingState { + var doneEnabled = true + if let editingName = editingState.editingName, editingName.isEmpty { + doneEnabled = false + } + if peer is TelegramChannel { + if (view.cachedData as? CachedChannelData) == nil { + doneEnabled = false + } + } + + if state.savingData { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: { + var updateValues: (title: String?, description: String?) = (nil, nil) + updateState { state in + updateValues = valuesRequiringUpdate(state: state, view: view) + if updateValues.0 != nil || updateValues.1 != nil { + return state.withUpdatedSavingData(true) + } else { + return state.withUpdatedEditingState(nil) + } + } + + let updateTitle: Signal + if let titleValue = updateValues.title { + updateTitle = updatePeerTitle(account: account, peerId: peerId, title: titleValue) + |> mapError { _ in return Void() } + } else { + updateTitle = .complete() + } + + let updateDescription: Signal + if let descriptionValue = updateValues.description { + updateDescription = updatePeerDescription(account: account, peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) + |> mapError { _ in return Void() } + } else { + updateDescription = .complete() + } + + let signal = combineLatest(updateTitle, updateDescription) + + updatePeerNameDisposable.set((signal |> deliverOnMainQueue).start(error: { _ in + updateState { state in + return state.withUpdatedSavingData(false) + } + }, completed: { + updateState { state in + return state.withUpdatedSavingData(false).withUpdatedEditingState(nil) + } + })) + }) + } + } else { + rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + if let peer = peer as? TelegramGroup { + updateState { state in + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName), editingDescriptionText: "")) + } + } else if let channel = peer as? TelegramChannel, case .group = channel.info { + var text = "" + if let cachedData = view.cachedData as? CachedChannelData, let about = cachedData.about { + text = about + } + updateState { state in + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel.indexName), editingDescriptionText: text)) + } + } + }) + } + + let controllerState = ItemListControllerState(title: "Info", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) + let listState = ItemListNodeState(entries: groupInfoEntries(account: account, view: view, state: state), style: .blocks) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + + pushControllerImpl = { [weak controller] value in + (controller?.navigationController as? NavigationController)?.pushViewController(value) + } + presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.present(value, in: .window, with: presentationArguments) + } + return controller +} diff --git a/TelegramUI/GroupInfoEntries.swift b/TelegramUI/GroupInfoEntries.swift index 94af3f078e..6f676fc079 100644 --- a/TelegramUI/GroupInfoEntries.swift +++ b/TelegramUI/GroupInfoEntries.swift @@ -1,4 +1,4 @@ -import Foundation +/*import Foundation import Postbox import TelegramCore import SwiftSignalKit @@ -674,3 +674,4 @@ func groupInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) } +*/ diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 78f1fc1af4..c4e7e76137 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -6,6 +6,7 @@ import SwiftSignalKit enum ItemListActionKind { case generic case destructive + case neutral } enum ItemListActionAlignment { @@ -110,9 +111,17 @@ class ItemListActionItemNode: ListViewItemNode { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) 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 insets: UIEdgeInsets diff --git a/TelegramUI/ItemListActivityTextItem.swift b/TelegramUI/ItemListActivityTextItem.swift new file mode 100644 index 0000000000..cf97e1bb51 --- /dev/null +++ b/TelegramUI/ItemListActivityTextItem.swift @@ -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)) -> 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) + } +} diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index b2d9a3e141..23789d7edc 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -21,12 +21,27 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { var composedTitle: String { switch self { 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): 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 { switch lhs { case let .personName(firstName, lastName): diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index c54f0e06c4..36453105df 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -5,10 +5,11 @@ import SwiftSignalKit enum ItemListNavigationButtonStyle { case regular case bold + case activity var barButtonItemStyle: UIBarButtonItemStyle { switch self { - case .regular: + case .regular, .activity: return .plain case .bold: return .done @@ -27,6 +28,14 @@ struct ItemListControllerState { let title: String let leftNavigationButton: 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: ViewController { @@ -73,7 +82,12 @@ final class ItemListController: ViewController { if strongSelf.rightNavigationButtonTitleAndStyle?.0 != controllerState.rightNavigationButton?.title || strongSelf.rightNavigationButtonTitleAndStyle?.1 != controllerState.rightNavigationButton?.style { 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.navigationItem.setRightBarButton(item, animated: false) item.isEnabled = rightNavigationButton.enabled @@ -112,6 +126,8 @@ final class ItemListController: ViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + (self.displayNode as! ItemListNode).listNode.preloadPages = true + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { if case .modalSheet = presentationArguments.presentationAnimation { (self.displayNode as! ItemListNode).animateIn() @@ -122,4 +138,16 @@ final class ItemListController: ViewController { func dismiss() { (self.displayNode as! ItemListNode).animateOut() } + + func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? { + var result: CGRect? + (self.displayNode as! ItemListNode).listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListViewItemNode { + if predicate(itemNode) { + result = itemNode.convert(itemNode.bounds, to: self.displayNode) + } + } + } + return result + } } diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 16e306f544..cfbd2e8cbc 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -39,11 +39,19 @@ private struct ItemListNodeTransition { let updateStyle: ItemListStyle? let firstTime: Bool let animated: Bool + let animateAlpha: Bool } struct ItemListNodeState { let entries: [Entry] 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: ASDisplayNode { @@ -53,7 +61,7 @@ final class ItemListNode: ASDisplayNode { } private var didSetReady = false - private let listNode: ListView + let listNode: ListView private let transitionDisposable = MetaDisposable() private var enqueuedTransitions: [ItemListNodeTransition] = [] @@ -81,7 +89,7 @@ final class ItemListNode: ASDisplayNode { if previous?.style != 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 if let strongSelf = self { strongSelf.enqueueTransition(transition) @@ -167,6 +175,9 @@ final class ItemListNode: ASDisplayNode { options.insert(.LowLatency) } else if transition.animated { 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 if let strongSelf = self { diff --git a/TelegramUI/ItemListEditableItem.swift b/TelegramUI/ItemListEditableItem.swift index c8ef0be863..47dcb392d9 100644 --- a/TelegramUI/ItemListEditableItem.swift +++ b/TelegramUI/ItemListEditableItem.swift @@ -123,6 +123,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { case .changed: var translation = recognizer.translation(in: self.view) translation.x += self.initialRevealOffset + translation.x = min(0.0, translation.x) if self.revealNode == nil && translation.x.isLess(than: 0.0) { self.setupAndAddRevealNode() self.revealOptionsInteractivelyOpened() diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index bb41b71685..0483518dbd 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -5,12 +5,14 @@ import SwiftSignalKit class ItemListMultilineInputItem: ListViewItem, ItemListItem { let text: String + let placeholder: String let sectionId: ItemListSectionId let action: () -> 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.placeholder = placeholder self.sectionId = sectionId self.textUpdated = textUpdated self.action = action @@ -120,6 +122,8 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega 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 @@ -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.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.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - leftInset - 8.0, height: textLayout.size.height)) } diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift new file mode 100644 index 0000000000..00d2412bd9 --- /dev/null +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -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)) -> 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) + } +} diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index 8287565cd9..089439cb6c 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -6,12 +6,14 @@ import SwiftSignalKit class ItemListPeerActionItem: ListViewItem, ItemListItem { let icon: UIImage? let title: String + let editing: Bool let sectionId: ItemListSectionId 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.title = title + self.editing = editing self.sectionId = sectionId self.action = action } @@ -25,7 +27,7 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem { node.insets = layout.insets completion(node, { - return (nil, { apply() }) + return (nil, { apply(false) }) }) } } @@ -35,11 +37,16 @@ class ItemListPeerActionItem: ListViewItem, ItemListItem { Queue.mainQueue().async { let makeLayout = node.asyncLayout() + var animated = true + if case .None = animation { + animated = false + } + async { let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { - apply() + apply(animated) }) } } @@ -99,13 +106,15 @@ class ItemListPeerActionItemNode: ListViewItemNode { 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) return { item, width, neighbors in 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 @@ -115,13 +124,20 @@ class ItemListPeerActionItemNode: ListViewItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - return (layout, { [weak self] in + return (layout, { [weak self] animated in if let strongSelf = self { let _ = titleApply() + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + strongSelf.iconNode.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 { @@ -139,11 +155,12 @@ class ItemListPeerActionItemNode: ListViewItemNode { default: strongSelf.topStripeNode.isHidden = false } + let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = leftInset + bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight default: 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.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)) } diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 74018d0fec..16d5274ac7 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -5,21 +5,55 @@ import SwiftSignalKit import Postbox 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 { let account: Account - let peer: Peer? + let peer: Peer let presence: PeerPresence? + let text: ItemListPeerItemText let label: String? + let editing: ItemListPeerItemEditing + let enabled: Bool let sectionId: ItemListSectionId 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.peer = peer self.presence = presence + self.text = text self.label = label + self.editing = editing + self.enabled = enabled self.sectionId = sectionId 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)) -> Void) { @@ -31,7 +65,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { node.insets = layout.insets completion(node, { - return (nil, { apply() }) + return (nil, { apply(false) }) }) } } @@ -41,11 +75,16 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { Queue.mainQueue().async { let makeLayout = node.asyncLayout() + var animated = true + if case .None = animation { + animated = false + } + async { let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { - apply() + apply(animated) }) } } @@ -72,6 +111,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? private let avatarNode: AvatarNode private let titleNode: TextNode @@ -81,7 +121,12 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ItemListPeerItem, CGFloat, ItemListNeighbors)? + private var editableControlNode: ItemListEditableControlNode? + override var canBeSelected: Bool { + if self.editableControlNode != nil || self.disabledOverlayNode != nil { + return false + } if let item = self.layoutParams?.0, item.action != nil { return true } else { @@ -134,37 +179,53 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { 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 makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + + var currentDisabledOverlayNode = self.disabledOverlayNode return { item, width, neighbors in var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? var labelAttributedString: NSAttributedString? - if let peer = item.peer { - if let user = peer as? TelegramUser { - if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { - let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) - 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)) - } - + let peerRevealOptions: [ItemListRevealOption] + if item.editing.editable && item.enabled { + peerRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(0xff3824))] + } else { + peerRevealOptions = [] + } + + if let user = item.peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) + 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 { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) @@ -172,11 +233,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } else { statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) } - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) - } + case let .text(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6)) } if let label = item.label { @@ -185,10 +243,21 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { 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 (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) + let editingOffset: CGFloat + 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 contentSize = CGSize(width: width, height: 48.0) @@ -197,12 +266,73 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) 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 { strongSelf.layoutParams = (item, width, neighbors) 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 _ = statusApply() let _ = labelApply() @@ -228,30 +358,31 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): - bottomStripeInset = leftInset + bottomStripeInset = leftInset + editingOffset 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)) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset, y: 5.0), size: titleLayout.size) - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset, 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.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 5.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: statusLayout.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)) - if let peer = item.peer { - strongSelf.avatarNode.setPeer(account: item.account, peer: peer) - } + 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))) + strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) 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 { 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) { 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) + } } } diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift new file mode 100644 index 0000000000..4f553065a2 --- /dev/null +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -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)) -> 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("") + } + } + } +} diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index f46f96c462..79a3f5f0e5 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -76,13 +76,11 @@ class ItemListTextItemNode: ListViewItemNode { let contentSize: CGSize let insets: UIEdgeInsets - let separatorHeight = UIScreenPixel contentSize = CGSize(width: width, height: titleLayout.size.height + verticalInset + verticalInset) insets = itemListNeighborsPlainInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/ManagedAudioRecorder.swift b/TelegramUI/ManagedAudioRecorder.swift index 172b1af61b..59bb738c07 100644 --- a/TelegramUI/ManagedAudioRecorder.swift +++ b/TelegramUI/ManagedAudioRecorder.swift @@ -124,7 +124,7 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U } } else { 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) if status != noErr { - trace("ManagedAudioRecorder", what: "AudioOutputUnitStop returned \(status)") + Logger.shared.log("ManagedAudioRecorder", "AudioOutputUnitStop returned \(status)") } status = AudioUnitUninitialize(audioUnit) if status != noErr { - trace("ManagedAudioRecorder", what: "AudioUnitUninitialize returned \(status)") + Logger.shared.log("ManagedAudioRecorder", "AudioUnitUninitialize returned \(status)") } status = AudioComponentInstanceDispose(audioUnit) if status != noErr { - trace("ManagedAudioRecorder", what: "AudioComponentInstanceDispose returned \(status)") + Logger.shared.log("ManagedAudioRecorder", "AudioComponentInstanceDispose returned \(status)") } } diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index 2bf371543d..926a820bf6 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -322,12 +322,12 @@ private final class AudioPlayerRendererContext { status = AudioOutputUnitStop(audioUnit) if status != noErr { - trace("AudioPlayerRenderer", what: "AudioOutputUnitStop error \(status)") + Logger.shared.log("AudioPlayerRenderer", "AudioOutputUnitStop error \(status)") } status = AudioComponentInstanceDispose(audioUnit); if status != noErr { - trace("AudioPlayerRenderer", what: "AudioComponentInstanceDispose error \(status)") + Logger.shared.log("AudioPlayerRenderer", "AudioComponentInstanceDispose error \(status)") } self.audioUnit = nil } diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index f159e4867a..a1753ae194 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -263,7 +263,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in if let value = value { - arguments?.updateMessageSound(value) + arguments?.updateGroupSound(value) } })) }) diff --git a/TelegramUI/PeerInfoController.swift b/TelegramUI/PeerInfoController.swift index eee70b434f..3ad081224f 100644 --- a/TelegramUI/PeerInfoController.swift +++ b/TelegramUI/PeerInfoController.swift @@ -4,6 +4,21 @@ import Postbox import SwiftSignalKit 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 { let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void let openSharedMedia: () -> Void diff --git a/TelegramUI/PeerInfoEntries.swift b/TelegramUI/PeerInfoEntries.swift index 6b9bb7498f..2b408fe155 100644 --- a/TelegramUI/PeerInfoEntries.swift +++ b/TelegramUI/PeerInfoEntries.swift @@ -48,7 +48,7 @@ struct 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) } else if let secretChat = view.peers[view.peerId] as? TelegramSecretChat { 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 { return groupInfoEntries(view: view, state: state) - } + }*/ return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index df7df4f8b7..ee7204c1be 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -1,75 +1,405 @@ import Foundation import Display -import Postbox import SwiftSignalKit +import Postbox import TelegramCore -public class SettingsController: ListController { - private let account: Account +private struct SettingsItemArguments { + let account: Account + let accountManager: AccountManager - private let peer = Promise() - private let connectionStatus = Promise(.Online) - private let peerAndConnectionStatusDisposable = MetaDisposable() - - public init(account: Account) { - 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) - } + let pushController: (ViewController) -> Void + let presentController: (ViewController) -> Void + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let saveEditingState: () -> Void + let logout: () -> Void +} - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") +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 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 { - peerAndConnectionStatusDisposable.dispose() + 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 .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.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 +} diff --git a/TelegramUI/SettingsControllerEntries.swift b/TelegramUI/SettingsControllerEntries.swift deleted file mode 100644 index c6f76e23b5..0000000000 --- a/TelegramUI/SettingsControllerEntries.swift +++ /dev/null @@ -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.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 -} diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 5c0b45a307..29ec6c8298 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -10,9 +10,12 @@ public final class TelegramApplicationContext { let sharedChatMediaInputNode = Atomic(value: nil) let mediaManager = MediaManager() - public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void) { + public let applicationInForeground: Signal + + public init(openUrl: @escaping (String) -> Void, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal) { self.openUrl = openUrl self.getTopWindow = getTopWindow self.displayNotification = displayNotification + self.applicationInForeground = applicationInForeground } } diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index e382324cc5..53d3de457b 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -184,7 +184,7 @@ final class TextNode: ASDisplayNode { var lineAdditionalWidth: CGFloat = 0.0 if cutoutEnabled { - if lineOriginY < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { + if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) lineCutoutOffset = cutoutOffset lineAdditionalWidth = cutoutWidth