From 7485eb3a8ec3ded395b9de2076155b2156f53ffe Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 3 Mar 2017 13:48:11 +0400 Subject: [PATCH] no message --- TelegramUI.xcodeproj/project.pbxproj | 68 +- TelegramUI/ChannelAdminsController.swift | 9 +- TelegramUI/ChannelBlacklistController.swift | 17 +- TelegramUI/ChannelInfoController.swift | 595 ++++++++++++++++++ TelegramUI/ChannelInfoEntries.swift | 237 ------- TelegramUI/ChannelMembersController.swift | 435 +++++++++++++ TelegramUI/ChannelVisibilityController.swift | 57 +- TelegramUI/ChatController.swift | 362 +++++++++-- TelegramUI/ChatControllerNode.swift | 26 +- TelegramUI/ChatDocumentGalleryItem.swift | 2 +- TelegramUI/ChatHistoryGridNode.swift | 6 +- TelegramUI/ChatHistoryListNode.swift | 52 +- TelegramUI/ChatHistoryNode.swift | 1 + TelegramUI/ChatHistoryViewForLocation.swift | 42 +- TelegramUI/ChatInputContextPanelNode.swift | 6 + TelegramUI/ChatInterfaceInputContexts.swift | 4 + TelegramUI/ChatInterfaceState.swift | 35 +- .../ChatInterfaceStateAccessoryPanels.swift | 9 + .../ChatInterfaceStateContextMenus.swift | 35 +- .../ChatInterfaceStateContextQueries.swift | 31 + .../ChatInterfaceStateInputPanels.swift | 66 +- TelegramUI/ChatInterfaceTitlePanelNodes.swift | 39 +- TelegramUI/ChatListItem.swift | 123 ++-- .../ChatListSearchRecentPeersNode.swift | 2 +- TelegramUI/ChatMessageBubbleItemNode.swift | 4 +- .../ChatMessageInteractiveFileNode.swift | 28 +- .../ChatMessageInteractiveMediaNode.swift | 4 +- TelegramUI/ChatMessageItem.swift | 3 +- TelegramUI/ChatMessageReplyInfoNode.swift | 1 - TelegramUI/ChatMessageSelectionNode.swift | 2 + .../ChatPanelInterfaceInteraction.swift | 20 +- .../ChatPinnedMessageTitlePanelNode.swift | 183 ++++++ .../ChatPresentationInterfaceState.swift | 83 ++- TelegramUI/ChatReportPeerTitlePanelNode.swift | 142 +++++ TelegramUI/ChatTextInputPanelNode.swift | 8 +- TelegramUI/ChatUnblockInputPanelNode.swift | 84 +++ TelegramUI/ComposeController.swift | 25 + TelegramUI/ContactSelectionController.swift | 39 +- .../ConvertToSupergroupController.swift | 151 +++++ TelegramUI/CreateChannelController.swift | 234 +++++++ TelegramUI/CreateGroupController.swift | 41 +- TelegramUI/DeleteChatInputPanelNode.swift | 51 ++ .../FFMpegMediaFrameSourceContext.swift | 2 +- TelegramUI/GenerateTextEntities.swift | 12 +- TelegramUI/GroupAdminsController.swift | 289 +++++++++ TelegramUI/GroupInfoController.swift | 118 +++- ...rizontalStickersChatContextPanelNode.swift | 1 + TelegramUI/ItemListActionItem.swift | 3 + TelegramUI/ItemListAvatarAndNameItem.swift | 5 +- TelegramUI/ItemListController.swift | 5 +- TelegramUI/ItemListDisclosureItem.swift | 1 - TelegramUI/ItemListMultilineInputItem.swift | 36 +- TelegramUI/ItemListPeerItem.swift | 19 +- TelegramUI/PeerInfoController.swift | 6 +- .../PeerMediaCollectionController.swift | 7 + TelegramUI/PhotoResources.swift | 2 +- .../PreparedChatHistoryViewTransition.swift | 4 +- ...retChatHandshakeStatusInputPanelNode.swift | 64 ++ TelegramUI/SettingsController.swift | 4 +- TelegramUI/StringWithAppliedEntities.swift | 14 +- .../TransformOutgoingMessageMedia.swift | 67 ++ TelegramUI/UserInfoController.swift | 572 +++++++++++++++++ TelegramUI/UserInfoEntries.swift | 361 +---------- TelegramUI/UsernameSetupController.swift | 300 +++++++++ .../WebpagePreviewAccessoryPanelNode.swift | 124 ++++ 65 files changed, 4504 insertions(+), 874 deletions(-) create mode 100644 TelegramUI/ChannelInfoController.swift delete mode 100644 TelegramUI/ChannelInfoEntries.swift create mode 100644 TelegramUI/ChannelMembersController.swift create mode 100644 TelegramUI/ChatPinnedMessageTitlePanelNode.swift create mode 100644 TelegramUI/ChatReportPeerTitlePanelNode.swift create mode 100644 TelegramUI/ChatUnblockInputPanelNode.swift create mode 100644 TelegramUI/ConvertToSupergroupController.swift create mode 100644 TelegramUI/CreateChannelController.swift create mode 100644 TelegramUI/DeleteChatInputPanelNode.swift create mode 100644 TelegramUI/GroupAdminsController.swift create mode 100644 TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift create mode 100644 TelegramUI/TransformOutgoingMessageMedia.swift create mode 100644 TelegramUI/UserInfoController.swift create mode 100644 TelegramUI/UsernameSetupController.swift create mode 100644 TelegramUI/WebpagePreviewAccessoryPanelNode.swift diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 2f267b64c1..a3bcd8b32c 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -23,8 +23,10 @@ D00C7CE91E379B820080C3D5 /* ChatSecretAutoremoveTimerActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C7CE81E379B820080C3D5 /* ChatSecretAutoremoveTimerActionSheet.swift */; }; D00C7CF71E37BF680080C3D5 /* SecretChatKeyVisualization.h in Headers */ = {isa = PBXBuildFile; fileRef = D00C7CF51E37BF680080C3D5 /* SecretChatKeyVisualization.h */; }; D00C7CF81E37BF680080C3D5 /* SecretChatKeyVisualization.m in Sources */ = {isa = PBXBuildFile; fileRef = D00C7CF61E37BF680080C3D5 /* SecretChatKeyVisualization.m */; }; + D00DBBDD1E65650800DB5485 /* ChatReportPeerTitlePanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00DBBDC1E65650800DB5485 /* ChatReportPeerTitlePanelNode.swift */; }; D00E15261DDBD4E700ACF65C /* LegacyCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */; }; D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; }; + D0127A0D1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0127A0C1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift */; }; D017494E1E1059570057C89A /* StringWithAppliedEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D017494D1E1059570057C89A /* StringWithAppliedEntities.swift */; }; D01749511E1067E40057C89A /* HashtagSearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01749501E1067E40057C89A /* HashtagSearchController.swift */; }; D01749531E1068820057C89A /* HashtagSearchControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01749521E1068820057C89A /* HashtagSearchControllerNode.swift */; }; @@ -34,6 +36,8 @@ D0177B801DFAE18500A5083A /* MediaPlayerTimeTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0177B7F1DFAE18500A5083A /* MediaPlayerTimeTextNode.swift */; }; D0177B821DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */; }; D0177B841DFB095000A5083A /* FileMediaResourceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */; }; + D018D3321E6460B300C5E089 /* ChatUnblockInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */; }; + D018D3351E6489EC00C5E089 /* CreateChannelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018D3341E6489EC00C5E089 /* CreateChannelController.swift */; }; D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */; }; D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; }; D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */; }; @@ -84,6 +88,7 @@ D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; }; D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0761D9190EF000889C2 /* GridMessageItem.swift */; }; D03120F61DA534C1006A2A60 /* ItemListActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03120F51DA534C1006A2A60 /* ItemListActionItem.swift */; }; + D033FEAB1E61BFC100644997 /* GroupAdminsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */; }; D03922A71DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03922A61DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift */; }; D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */; }; D039EB081DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */; }; @@ -94,6 +99,7 @@ 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 */; }; + D04662811E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04662801E68BA64006FAFC4 /* TransformOutgoingMessageMedia.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 */; }; @@ -173,6 +179,11 @@ 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 */; }; + D0528E561E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0528E551E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift */; }; + D0528E581E65773300E2FEF5 /* DeleteChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0528E571E65773300E2FEF5 /* DeleteChatInputPanelNode.swift */; }; + D0528E631E65BECA00E2FEF5 /* UserInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */; }; + D0528E681E65CB2C00E2FEF5 /* UsernameSetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */; }; + D0528E6D1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0528E6C1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.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 */; }; @@ -180,6 +191,9 @@ 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 */; }; + D0613FC81E5F8AB100202CDB /* ChannelInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */; }; + D0613FCD1E60482300202CDB /* ChannelMembersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FCC1E60482300202CDB /* ChannelMembersController.swift */; }; + D0613FD51E6064D200202CDB /* ConvertToSupergroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */; }; D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */; }; @@ -245,7 +259,6 @@ D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */; }; D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */; }; D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */; }; - D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */; }; D0B843D91DAAAA0C005F29E1 /* ItemListPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */; }; D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */; }; D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; }; @@ -486,8 +499,10 @@ D00C7CE81E379B820080C3D5 /* ChatSecretAutoremoveTimerActionSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatSecretAutoremoveTimerActionSheet.swift; sourceTree = ""; }; D00C7CF51E37BF680080C3D5 /* SecretChatKeyVisualization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SecretChatKeyVisualization.h; sourceTree = ""; }; D00C7CF61E37BF680080C3D5 /* SecretChatKeyVisualization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecretChatKeyVisualization.m; sourceTree = ""; }; + D00DBBDC1E65650800DB5485 /* ChatReportPeerTitlePanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatReportPeerTitlePanelNode.swift; sourceTree = ""; }; D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCamera.swift; sourceTree = ""; }; D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; + D0127A0C1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPinnedMessageTitlePanelNode.swift; sourceTree = ""; }; D017494D1E1059570057C89A /* StringWithAppliedEntities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringWithAppliedEntities.swift; sourceTree = ""; }; D01749501E1067E40057C89A /* HashtagSearchController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagSearchController.swift; sourceTree = ""; }; D01749521E1068820057C89A /* HashtagSearchControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagSearchControllerNode.swift; sourceTree = ""; }; @@ -497,6 +512,8 @@ D0177B7F1DFAE18500A5083A /* MediaPlayerTimeTextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerTimeTextNode.swift; sourceTree = ""; }; D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaNavigationAccessoryItemListNode.swift; sourceTree = ""; }; D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMediaResourceStatus.swift; sourceTree = ""; }; + D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnblockInputPanelNode.swift; sourceTree = ""; }; + D018D3341E6489EC00C5E089 /* CreateChannelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateChannelController.swift; sourceTree = ""; }; D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionButtonsNode.swift; sourceTree = ""; }; 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 = ""; }; @@ -547,6 +564,7 @@ D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; D03120F51DA534C1006A2A60 /* ItemListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActionItem.swift; sourceTree = ""; }; + D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAdminsController.swift; sourceTree = ""; }; D03922A61DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerScrubbingNode.swift; sourceTree = ""; }; D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = ""; }; @@ -557,6 +575,7 @@ 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 = ""; }; + D04662801E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOutgoingMessageMedia.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 = ""; }; @@ -636,6 +655,11 @@ 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 = ""; }; + D0528E551E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatHandshakeStatusInputPanelNode.swift; sourceTree = ""; }; + D0528E571E65773300E2FEF5 /* DeleteChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteChatInputPanelNode.swift; sourceTree = ""; }; + D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoController.swift; sourceTree = ""; }; + D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameSetupController.swift; sourceTree = ""; }; + D0528E6C1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebpagePreviewAccessoryPanelNode.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 = ""; }; @@ -643,6 +667,9 @@ 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 = ""; }; + D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoController.swift; sourceTree = ""; }; + D0613FCC1E60482300202CDB /* ChannelMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersController.swift; sourceTree = ""; }; + D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioPlaylistPlayer.swift; sourceTree = ""; }; @@ -711,7 +738,6 @@ D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoController.swift; sourceTree = ""; }; D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = ""; }; D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = ""; }; - D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = ""; }; D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerItem.swift; sourceTree = ""; }; 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 = ""; }; @@ -982,6 +1008,14 @@ name = "Hashtag Search"; sourceTree = ""; }; + D018D3331E6489D700C5E089 /* Create Channel */ = { + isa = PBXGroup; + children = ( + D018D3341E6489EC00C5E089 /* CreateChannelController.swift */, + ); + name = "Create Channel"; + sourceTree = ""; + }; D01B27931E38F3920022A4C0 /* Item List */ = { isa = PBXGroup; children = ( @@ -1052,6 +1086,7 @@ D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */, D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */, D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */, + D0528E6C1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.swift */, ); name = "Accessory Panels"; sourceTree = ""; @@ -1438,6 +1473,9 @@ D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */, D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */, D01749561E1087CC0057C89A /* ChatBotStartInputPanelNode.swift */, + D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */, + D0528E551E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift */, + D0528E571E65773300E2FEF5 /* DeleteChatInputPanelNode.swift */, ); name = "Input Panels"; sourceTree = ""; @@ -1457,6 +1495,7 @@ D087751A1E3F540900A97350 /* Contact Multiselection */, D0BC387D1E40F1B90044D6FE /* Contact Selection */, D0BC38671E3FB9190044D6FE /* Create Group */, + D018D3331E6489D700C5E089 /* Create Channel */, ); name = Compose; sourceTree = ""; @@ -1487,6 +1526,7 @@ D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */, D0CE1BD21E51BC6100404327 /* DebugController.swift */, D03E5E081E55C49C0029569A /* DebugAccountsController.swift */, + D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */, ); name = Settings; sourceTree = ""; @@ -1570,7 +1610,9 @@ isa = PBXGroup; children = ( D0D2686B1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift */, + D0127A0C1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift */, D02383721DDF0D8A004018B6 /* ChatInfoTitlePanelNode.swift */, + D00DBBDC1E65650800DB5485 /* ChatReportPeerTitlePanelNode.swift */, D02383781DDF1A4D004018B6 /* ChatRequestInProgressTitlePanelNode.swift */, D023837D1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift */, ); @@ -1729,12 +1771,16 @@ children = ( D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */, D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */, - D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, D0486F091E523C8500091F0C /* GroupInfoController.swift */, D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */, D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */, D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */, + D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */, + D0613FCC1E60482300202CDB /* ChannelMembersController.swift */, + D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */, + D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */, + D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */, ); name = Controller; sourceTree = ""; @@ -2139,6 +2185,7 @@ D0F69EA01D6B8E380046BCD6 /* StickerResources.swift */, D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */, D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */, + D04662801E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift */, ); name = Resources; sourceTree = ""; @@ -2404,6 +2451,7 @@ D0DA44561E4E7F43005FDCA7 /* ShakeAnimation.swift in Sources */, D0561DE61E57424700E6B9E9 /* ItemListMultilineTextItem.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, + D018D3321E6460B300C5E089 /* ChatUnblockInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, D039EB031DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */, @@ -2427,6 +2475,7 @@ D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */, D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, D0DC354C1DE366DE000195EB /* CommandChatInputPanelItem.swift in Sources */, + D0613FCD1E60482300202CDB /* ChannelMembersController.swift in Sources */, D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */, D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */, D0F7AB351DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift in Sources */, @@ -2463,6 +2512,7 @@ D0D03B131DECB0FE00220C46 /* framing.c in Sources */, D0F69E651D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift in Sources */, D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */, + D0613FC81E5F8AB100202CDB /* ChannelInfoController.swift in Sources */, D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */, D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */, @@ -2483,6 +2533,7 @@ D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0EFD8961DDE8249009E508A /* LegacyLocationPicker.swift in Sources */, + D0613FD51E6064D200202CDB /* ConvertToSupergroupController.swift in Sources */, D04BB3771E48797500650E93 /* RMLoginViewController.m in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0B843921DA7F13E005F29E1 /* ItemListDisclosureItem.swift in Sources */, @@ -2502,6 +2553,7 @@ D07551931DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift in Sources */, D039EB0A1DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, + D033FEAB1E61BFC100644997 /* GroupAdminsController.swift in Sources */, D00C7CDC1E3776E50080C3D5 /* SecretMediaPreviewController.swift in Sources */, D0215D3E1E041048001A0B1E /* InstantPageMedia.swift in Sources */, D00C7CD91E36B2DB0080C3D5 /* ContactListNode.swift in Sources */, @@ -2523,6 +2575,7 @@ D02383771DDF16B2004018B6 /* ChatControllerTitlePanelNodeContainer.swift in Sources */, D00370321DA46C06004308D3 /* ItemListTextWithLabelItem.swift in Sources */, D00B3FA21E3A983E003872C3 /* ItemListTextItem.swift in Sources */, + D018D3351E6489EC00C5E089 /* CreateChannelController.swift in Sources */, D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */, D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */, D099EA211DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, @@ -2577,6 +2630,7 @@ D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */, + D04662811E68BA64006FAFC4 /* TransformOutgoingMessageMedia.swift in Sources */, D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */, D0E305AF1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */, D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */, @@ -2603,11 +2657,13 @@ D04BB33E1E48797500650E93 /* platform_log.c in Sources */, D01B279B1E39386C0022A4C0 /* SettingsController.swift in Sources */, D08775201E3F595000A97350 /* ContactListActionItem.swift in Sources */, + D0528E631E65BECA00E2FEF5 /* UserInfoController.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 */, + D0528E581E65773300E2FEF5 /* DeleteChatInputPanelNode.swift in Sources */, D0215D381E040F53001A0B1E /* InstantPageNode.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0D03B101DECB0FE00220C46 /* wav_io.c in Sources */, @@ -2625,10 +2681,13 @@ D04BB2BE1E44FD2600650E93 /* AuthorizationSequenceCodeEntryController.swift in Sources */, D01B279F1E394BD70022A4C0 /* InAppNotificationSettings.swift in Sources */, D0F69E131D6B8ACF0046BCD6 /* ChatController.swift in Sources */, + D00DBBDD1E65650800DB5485 /* ChatReportPeerTitlePanelNode.swift in Sources */, D023837E1DDF50FD004018B6 /* ChatToastAlertPanelNode.swift in Sources */, D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */, D0E7A1C11D8C258D00C37A6F /* ChatHistoryEntriesForView.swift in Sources */, D01749511E1067E40057C89A /* HashtagSearchController.swift in Sources */, + D0528E561E65750600E2FEF5 /* SecretChatHandshakeStatusInputPanelNode.swift in Sources */, + D0528E681E65CB2C00E2FEF5 /* UsernameSetupController.swift in Sources */, D04BB3381E48797500650E93 /* rngs.c in Sources */, D04BB2C01E44FD3100650E93 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, D0D03B0E1DECB0FE00220C46 /* picture.c in Sources */, @@ -2644,7 +2703,6 @@ D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */, D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */, D00C7CDE1E37770A0080C3D5 /* SecretMediaPreviewControllerNode.swift in Sources */, - D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */, D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */, D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */, D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, @@ -2707,6 +2765,7 @@ D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */, D0215D5A1E04310C001A0B1E /* InstantPageTile.swift in Sources */, D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */, + D0127A0D1E6424AC003BFF2E /* ChatPinnedMessageTitlePanelNode.swift in Sources */, D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */, D0177B841DFB095000A5083A /* FileMediaResourceStatus.swift in Sources */, D03922A71DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift in Sources */, @@ -2727,6 +2786,7 @@ D0F69DCF1D6B8A0D0046BCD6 /* SearchBarNode.swift in Sources */, D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */, D00E15261DDBD4E700ACF65C /* LegacyCamera.swift in Sources */, + D0528E6D1E65DE3B00E2FEF5 /* WebpagePreviewAccessoryPanelNode.swift in Sources */, D00219061DDD1C9E00BE708A /* ImageContainingNode.swift in Sources */, D0F69E2E1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift in Sources */, D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */, diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index d606fc2f89..ba63b414b4 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -211,7 +211,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: peerText = "Moderator" } - return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: nil, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) @@ -627,6 +627,8 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon adminsPromise.set(adminsSignal) + var previousPeers: [RenderedChannelParticipant]? + let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { state, view, admins -> (ItemListControllerState, (ItemListNodeState, ChannelAdminsEntry.ItemGenerationArguments)) in @@ -647,8 +649,11 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } } + let previous = previousPeers + previousPeers = admins + 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: true) + let listState = ItemListNodeState(entries: ChannelAdminsControllerEntries(view: view, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index 8658e54b78..4b36279882 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -96,7 +96,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { func item(_ arguments: ChannelBlacklistControllerArguments) -> ListViewItem { switch self { case let .peerItem(_, participant, editing, enabled): - return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: nil, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -260,6 +260,8 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View peersPromise.set(peersSignal) + var previousPeers: [RenderedChannelParticipant]? + let signal = combineLatest(statePromise.get(), peerView, peersPromise.get()) |> deliverOnMainQueue |> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelBlacklistEntry.ItemGenerationArguments)) in @@ -283,14 +285,23 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View var emptyStateItem: ItemListControllerEmptyStateItem? if let peers = peers { if peers.isEmpty { - emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them.") + if let peer = view.peers[view.peerId] as? TelegramChannel { + if case .group = peer.info { + emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them.") + } else { + emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the channel and can only come back if invited by an admin. Invite links don't work for them.") + } + } } } else if peers == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() } + let previous = previousPeers + previousPeers = peers + let controllerState = ItemListControllerState(title: "Blacklist", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) - let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true) + let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift new file mode 100644 index 0000000000..36688d0bab --- /dev/null +++ b/TelegramUI/ChannelInfoController.swift @@ -0,0 +1,595 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ChannelInfoControllerArguments { + let account: Account + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updateEditingDescriptionText: (String) -> Void + let openChannelTypeSetup: () -> Void + let changeNotificationMuteSettings: () -> Void + let openSharedMedia: () -> Void + let openAdmins: () -> Void + let openMembers: () -> Void + let openBanned: () -> Void + let reportChannel: () -> Void + let leaveChannel: () -> Void + let deleteChannel: () -> Void + + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void) { + self.account = account + self.updateEditingName = updateEditingName + self.updateEditingDescriptionText = updateEditingDescriptionText + self.openChannelTypeSetup = openChannelTypeSetup + self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.openSharedMedia = openSharedMedia + self.openAdmins = openAdmins + self.openMembers = openMembers + self.openBanned = openBanned + self.reportChannel = reportChannel + self.leaveChannel = leaveChannel + self.deleteChannel = deleteChannel + } +} + +private enum ChannelInfoSection: ItemListSectionId { + case info + case sharedMediaAndNotifications + case members + case reportOrLeave +} + +private enum ChannelInfoEntry: ItemListNodeEntry { + case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) + case about(text: String) + case addressName(value: String) + case channelTypeSetup(isPublic: Bool) + case channelDescriptionSetup(text: String) + case admins(count: Int32) + case members(count: Int32) + case banned(count: Int32) + case notifications(settings: PeerNotificationSettings) + case sharedMedia + case report + case leave + case deleteChannel + + var section: ItemListSectionId { + switch self { + case .info, .about, .addressName, .channelTypeSetup, .channelDescriptionSetup: + return ChannelInfoSection.info.rawValue + case .admins, .members, .banned: + return ChannelInfoSection.members.rawValue + case .sharedMedia, .notifications: + return ChannelInfoSection.sharedMediaAndNotifications.rawValue + case .report, .leave, .deleteChannel: + return ChannelInfoSection.reportOrLeave.rawValue + } + } + + var stableId: Int32 { + switch self { + case .info: + return 0 + case .about: + return 1 + case .addressName: + return 2 + case .channelDescriptionSetup: + return 3 + case .channelTypeSetup: + return 4 + case .admins: + return 5 + case .members: + return 6 + case .banned: + return 7 + case .notifications: + return 8 + case .sharedMedia: + return 9 + case .report: + return 10 + case .leave: + return 11 + case .deleteChannel: + return 12 + } + } + + static func ==(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> 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 let .about(text): + if case .about(text) = rhs { + return true + } else { + return false + } + case let .addressName(value): + if case .addressName(value) = rhs { + return true + } else { + return false + } + case let .channelTypeSetup(isPublic): + if case .channelTypeSetup(isPublic) = rhs { + return true + } else { + return false + } + case let .channelDescriptionSetup(text): + if case .channelDescriptionSetup(text) = rhs { + return true + } else { + return false + } + case let .admins(count): + if case .admins(count) = rhs { + return true + } else { + return false + } + case let .members(count): + if case .members(count) = rhs { + return true + } else { + return false + } + case let .banned(count): + if case .banned(count) = rhs { + return true + } else { + return false + } + case .sharedMedia, .report, .leave, .deleteChannel: + return lhs.stableId == rhs.stableId + case let .notifications(lhsSettings): + if case let .notifications(rhsSettings) = rhs { + return lhsSettings.isEqual(to: rhsSettings) + } else { + return false + } + } + } + + static func <(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: ChannelInfoControllerArguments) -> 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: .plain, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }) + case let .about(text): + return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) + case let .addressName(value): + return ItemListTextWithLabelItem(label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section) + case let .channelTypeSetup(isPublic): + return ItemListDisclosureItem(title: "Channel Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .plain, action: { + arguments.openChannelTypeSetup() + }) + case let .channelDescriptionSetup(text): + return ItemListMultilineInputItem(text: text, placeholder: "Channel Description", sectionId: self.section, style: .plain, textUpdated: { updatedText in + arguments.updateEditingDescriptionText(updatedText) + }, action: { + + }) + case let .admins(count): + return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .plain, action: { + arguments.openAdmins() + }) + case let .members(count): + return ItemListDisclosureItem(title: "Members", label: "\(count)", sectionId: self.section, style: .plain, action: { + arguments.openMembers() + }) + case let .banned(count): + return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .plain, action: { + arguments.openBanned() + }) + case .sharedMedia: + return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { + arguments.openSharedMedia() + }) + 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: .plain, action: { + arguments.changeNotificationMuteSettings() + }) + case .report: + return ItemListActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.reportChannel() + }) + case .leave: + return ItemListActionItem(title: "Leave Channel", kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.leaveChannel() + }) + case .deleteChannel: + return ItemListActionItem(title: "Delete Channel", kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.deleteChannel() + }) + } + } +} + +private struct ChannelInfoState: Equatable { + let editingState: ChannelInfoEditingState? + let savingData: Bool + + init(editingState: ChannelInfoEditingState?, savingData: Bool) { + self.editingState = editingState + self.savingData = savingData + } + + init() { + self.editingState = nil + self.savingData = false + } + + static func ==(lhs: ChannelInfoState, rhs: ChannelInfoState) -> Bool { + if lhs.editingState != rhs.editingState { + return false + } + if lhs.savingData != rhs.savingData { + return false + } + return true + } + + func withUpdatedEditingState(_ editingState: ChannelInfoEditingState?) -> ChannelInfoState { + return ChannelInfoState(editingState: editingState, savingData: self.savingData) + } + + func withUpdatedSavingData(_ savingData: Bool) -> ChannelInfoState { + return ChannelInfoState(editingState: self.editingState, savingData: savingData) + } +} + +private struct ChannelInfoEditingState: Equatable { + let editingName: ItemListAvatarAndNameInfoItemName? + let editingDescriptionText: String + + func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> ChannelInfoEditingState { + return ChannelInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText) + } + + static func ==(lhs: ChannelInfoEditingState, rhs: ChannelInfoEditingState) -> Bool { + if lhs.editingName != rhs.editingName { + return false + } + if lhs.editingDescriptionText != rhs.editingDescriptionText { + return false + } + return true + } +} + +private func channelInfoEntries(account: Account, view: PeerView, state: ChannelInfoState) -> [ChannelInfoEntry] { + var entries: [ChannelInfoEntry] = [] + + if let peer = view.peers[view.peerId] as? TelegramChannel { + var canManageChannel = false + var canManageMembers = false + let isPublic = peer.username != nil + switch peer.role { + case .creator: + canManageChannel = true + canManageMembers = true + case .moderator: + canManageMembers = true + case .editor, .member: + break + } + + let infoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: nil) + entries.append(.info(peer: peer, cachedData: view.cachedData, state: infoState)) + + if let cachedChannelData = view.cachedData as? CachedChannelData { + if let editingState = state.editingState, canManageChannel { + entries.append(.channelDescriptionSetup(text: editingState.editingDescriptionText)) + } else { + if let about = cachedChannelData.about, !about.isEmpty { + entries.append(.about(text: about)) + } + } + } + + if state.editingState != nil && canManageChannel { + entries.append(.channelTypeSetup(isPublic: isPublic)) + } else if let username = peer.username, !username.isEmpty { + entries.append(.addressName(value: username)) + } + + if let cachedChannelData = view.cachedData as? CachedChannelData { + if state.editingState != nil && canManageMembers { + if let bannedCount = cachedChannelData.participantsSummary.bannedCount { + entries.append(.banned(count: bannedCount)) + } + } else if canManageMembers { + if let adminCount = cachedChannelData.participantsSummary.adminCount { + entries.append(.admins(count: adminCount)) + } + if let memberCount = cachedChannelData.participantsSummary.memberCount { + entries.append(.members(count: memberCount)) + } + } + } + + if let notificationSettings = view.notificationSettings { + entries.append(ChannelInfoEntry.notifications(settings: notificationSettings)) + } + entries.append(ChannelInfoEntry.sharedMedia) + + if peer.role == .creator { + if state.editingState != nil { + entries.append(ChannelInfoEntry.deleteChannel) + } + } else { + entries.append(ChannelInfoEntry.report) + if peer.participationStatus == .member { + entries.append(ChannelInfoEntry.leave) + } + } + } + + return entries +} + +private func valuesRequiringUpdate(state: ChannelInfoState, view: PeerView) -> (title: String?, description: String?) { + 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 channelInfoController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(ChannelInfoState(), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelInfoState()) + let updateState: ((ChannelInfoState) -> ChannelInfoState) -> 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 changeMuteSettingsDisposable = MetaDisposable() + actionsDisposable.add(changeMuteSettingsDisposable) + + let arguments = ChannelInfoControllerArguments(account: account, updateEditingName: { editingName in + updateState { state in + if let editingState = state.editingState { + return state.withUpdatedEditingState(ChannelInfoEditingState(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 + } + }, openChannelTypeSetup: { + presentControllerImpl?(channelVisibilityController(account: account, peerId: peerId, mode: .generic), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) + }, changeNotificationMuteSettings: { + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let notificationAction: (Int32) -> Void = { muteUntil in + let muteState: PeerMuteState + if muteUntil <= 0 { + muteState = .unmuted + } else if muteUntil == Int32.max { + muteState = .muted(until: Int32.max) + } else { + muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + } + changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Enable", action: { + dismissAction() + notificationAction(0) + }), + ActionSheetButtonItem(title: "Mute for 1 hour", action: { + dismissAction() + notificationAction(1 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 8 hours", action: { + dismissAction() + notificationAction(8 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 2 days", action: { + dismissAction() + notificationAction(2 * 24 * 60 * 60) + }), + ActionSheetButtonItem(title: "Disable", action: { + dismissAction() + notificationAction(Int32.max) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, openSharedMedia: { + if let controller = peerSharedMediaController(account: account, peerId: peerId) { + pushControllerImpl?(controller) + } + }, openAdmins: { + pushControllerImpl?(channelAdminsController(account: account, peerId: peerId)) + }, openMembers: { + pushControllerImpl?(channelMembersController(account: account, peerId: peerId)) + }, openBanned: { + pushControllerImpl?(channelBlacklistController(account: account, peerId: peerId)) + }, reportChannel: { + + }, leaveChannel: { + + }, deleteChannel: { + + }) + + let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) + |> map { state, view -> (ItemListControllerState, (ItemListNodeState, ChannelInfoEntry.ItemGenerationArguments)) in + let peer = peerViewMainPeer(view) + var leftNavigationButton: ItemListNavigationButton? + let rightNavigationButton: ItemListNavigationButton + if let editingState = state.editingState { + leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + updateState { + $0.withUpdatedEditingState(nil) + } + }) + + 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 channel = peer as? TelegramChannel, case .broadcast = channel.info { + var text = "" + if let cachedData = view.cachedData as? CachedChannelData, let about = cachedData.about { + text = about + } + updateState { state in + return state.withUpdatedEditingState(ChannelInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel.indexName), editingDescriptionText: text)) + } + } + }) + } + + let controllerState = ItemListControllerState(title: "Info", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) + let listState = ItemListNodeState(entries: channelInfoEntries(account: account, view: view, state: state), style: .plain) + + 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/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift deleted file mode 100644 index 36d4762e6e..0000000000 --- a/TelegramUI/ChannelInfoEntries.swift +++ /dev/null @@ -1,237 +0,0 @@ -/*import Foundation -import Postbox -import TelegramCore -import SwiftSignalKit -import Display - -private enum ChannelInfoSection: ItemListSectionId { - case info - case sharedMediaAndNotifications - case members - case reportOrLeave -} - -enum ChannelInfoEntry: PeerInfoEntry { - case info(peer: Peer?, cachedData: CachedPeerData?) - case about(text: String) - case userName(value: String) - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case report - case member(index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus) - case leave - - var section: ItemListSectionId { - switch self { - case .info, .about, .userName: - return ChannelInfoSection.info.rawValue - case .sharedMedia, .notifications: - return ChannelInfoSection.sharedMediaAndNotifications.rawValue - case .member: - return ChannelInfoSection.members.rawValue - case .report, .leave: - return ChannelInfoSection.reportOrLeave.rawValue - } - } - - var stableId: PeerInfoEntryStableId { - return IntPeerInfoEntryStableId(value: self.sortIndex) - } - - func isEqual(to: PeerInfoEntry) -> Bool { - guard let entry = to as? ChannelInfoEntry else { - return false - } - switch self { - case let .info(lhsPeer, lhsCachedData): - switch entry { - case let .info(rhsPeer, rhsCachedData): - 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 - } - return true - default: - return false - } - case let .about(lhsText): - switch entry { - case let .about(lhsText): - return true - default: - return false - } - case let .userName(value): - switch entry { - case .userName(value): - return true - default: - return false - } - case .sharedMedia: - switch entry { - case .sharedMedia: - return true - default: - return false - } - case let .notifications(lhsSettings): - switch entry { - case let .notifications(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus): - if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus) = entry, lhsIndex == rhsIndex && lhsPeerId == rhsPeerId, lhsMemberStatus == rhsMemberStatus { - if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else if (lhsPeer == nil) != (rhsPeer != nil) { - return false - } - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - if !lhsPresence.isEqual(to: rhsPresence) { - return false - } - } else if (lhsPresence == nil) != (rhsPresence != nil) { - return false - } - return true - } else { - return false - } - case .report: - switch entry { - case .report: - return true - default: - return false - } - case .leave: - switch entry { - case .leave: - return true - default: - return false - } - } - } - - private var sortIndex: Int { - switch self { - case .info: - return 0 - case .about: - return 1 - case .userName: - return 2 - case .sharedMedia: - return 3 - case .notifications: - return 4 - case let .member(index, _, _, _, _): - return 100 + index - case .report: - return 1001 - case .leave: - return 1002 - } - } - - func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool { - guard let entry = entry as? ChannelInfoEntry else { - return false - } - return self.sortIndex < entry.sortIndex - } - - func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { - switch self { - case let .info(peer, cachedData): - return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: nil, cachedData: cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: nil, updatingName: nil), sectionId: self.section, style: .plain, editingNameUpdated: { editingName in - - }) - case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) - case let .userName(value): - return ItemListTextWithLabelItem(label: "share link", text: "https://telegram.me/\(value)", multiline: false, sectionId: self.section) - return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - case .sharedMedia: - return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { - interaction.openSharedMedia() - }) - 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: .plain, action: { - interaction.changeNotificationMuteSettings() - }) - case let .member(_, peerId, peer, presence, memberStatus): - let label: String? - switch memberStatus { - case .admin: - label = "admin" - case .member: - label = nil - } - return ItemListPeerItem(account: account, peer: peer, presence: presence, label: label, sectionId: self.section, action: { - if let peer = peer { - interaction.openPeerInfo(peer.id) - } - }) - case .report: - return ItemListActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - case .leave: - return ItemListActionItem(title: "Leave Channel", kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - } - } -} - -func channelBroadcastInfoEntries(view: PeerView) -> PeerInfoEntries { - var entries: [PeerInfoEntry] = [] - entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) - if let cachedChannelData = view.cachedData as? CachedChannelData { - if let about = cachedChannelData.about, !about.isEmpty { - entries.append(ChannelInfoEntry.about(text: about)) - } - } - if let channel = view.peers[view.peerId] as? TelegramChannel { - if let username = channel.username, !username.isEmpty { - entries.append(ChannelInfoEntry.userName(value: username)) - } - entries.append(ChannelInfoEntry.sharedMedia) - entries.append(ChannelInfoEntry.notifications(settings: view.notificationSettings)) - entries.append(ChannelInfoEntry.report) - if channel.participationStatus == .member { - entries.append(ChannelInfoEntry.leave) - } - } - return PeerInfoEntries(entries: entries, leftNavigationButton: nil, rightNavigationButton: nil) -}*/ diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift new file mode 100644 index 0000000000..188292e82e --- /dev/null +++ b/TelegramUI/ChannelMembersController.swift @@ -0,0 +1,435 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ChannelMembersControllerArguments { + let account: Account + + let addMember: () -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let removePeer: (PeerId) -> Void + + init(account: Account, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) { + self.account = account + self.addMember = addMember + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.removePeer = removePeer + } +} + +private enum ChannelMembersSection: Int32 { + case addMembers + case peers +} + +private enum ChannelMembersEntryStableId: 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: ChannelMembersEntryStableId, rhs: ChannelMembersEntryStableId) -> 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 ChannelMembersEntry: ItemListNodeEntry { + case addMember + case addMemberInfo + case peerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + + var section: ItemListSectionId { + switch self { + case .addMember, .addMemberInfo: + return ChannelMembersSection.addMembers.rawValue + case .peerItem: + return ChannelMembersSection.peers.rawValue + } + } + + var stableId: ChannelMembersEntryStableId { + switch self { + case .addMember: + return .index(0) + case .addMemberInfo: + return .index(1) + case let .peerItem(_, participant, _, _): + return .peer(participant.peer.id) + } + } + + static func ==(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { + switch lhs { + case .addMember, .addMemberInfo: + return lhs.stableId == rhs.stableId + case let .peerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): + if case let .peerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsParticipant != rhsParticipant { + return false + } + if lhsEditing != rhsEditing { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { + switch lhs { + case .addMember: + return true + case .addMemberInfo: + switch rhs { + case .addMember: + return false + default: + return true + } + case let .peerItem(index, _, _, _): + switch rhs { + case let .peerItem(rhsIndex, _, _, _): + return index < rhsIndex + case .addMember, .addMemberInfo: + return false + } + } + } + + func item(_ arguments: ChannelMembersControllerArguments) -> ListViewItem { + switch self { + case .addMember: + return ItemListActionItem(title: "Add Members", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.addMember() + }) + case .addMemberInfo: + return ItemListTextItem(text: "Only channel admins can see this list.", sectionId: self.section) + case let .peerItem(_, participant, editing, enabled): + return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: nil, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + arguments.setPeerIdWithRevealedOptions(previousId, id) + }, removePeer: { peerId in + arguments.removePeer(peerId) + }) + } + } +} + +private struct ChannelMembersControllerState: Equatable { + let editing: Bool + let peerIdWithRevealedOptions: PeerId? + let removingPeerId: PeerId? + + init() { + self.editing = false + self.peerIdWithRevealedOptions = nil + self.removingPeerId = nil + } + + init(editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?) { + self.editing = editing + self.peerIdWithRevealedOptions = peerIdWithRevealedOptions + self.removingPeerId = removingPeerId + } + + static func ==(lhs: ChannelMembersControllerState, rhs: ChannelMembersControllerState) -> Bool { + if lhs.editing != rhs.editing { + return false + } + if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { + return false + } + if lhs.removingPeerId != rhs.removingPeerId { + return false + } + + return true + } + + func withUpdatedEditing(_ editing: Bool) -> ChannelMembersControllerState { + return ChannelMembersControllerState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId) + } + + func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelMembersControllerState { + return ChannelMembersControllerState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId) + } + + func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelMembersControllerState { + return ChannelMembersControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId) + } +} + +private func ChannelMembersControllerEntries(account: Account, view: PeerView, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelMembersEntry] { + var entries: [ChannelMembersEntry] = [] + + if let participants = participants { + entries.append(.addMember) + entries.append(.addMemberInfo) + + 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 + var isCreator = false + if let peer = view.peers[view.peerId] as? TelegramChannel { + isCreator = peer.role == .creator + } + + if participant.peer.id == account.peerId { + editable = false + } else { + if case .creator = participant.participant { + editable = false + } else if case .moderator = participant.participant { + editable = isCreator + } else if case .editor = participant.participant { + editable = isCreator + } + } + entries.append(.peerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) + index += 1 + } + } + + return entries +} + +public func channelMembersController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(ChannelMembersControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelMembersControllerState()) + let updateState: ((ChannelMembersControllerState) -> ChannelMembersControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + let actionsDisposable = DisposableSet() + + let addMembersDisposable = MetaDisposable() + actionsDisposable.add(addMembersDisposable) + + let removePeerDisposable = MetaDisposable() + actionsDisposable.add(removePeerDisposable) + + let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) + + let arguments = ChannelMembersControllerArguments(account: account, 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 + |> mapError { _ -> AddPeerMemberError in return .generic } + |> deliverOnMainQueue + |> mapToSignal { memberId -> Signal in + if let memberId = memberId { + let applyMembers: Signal = peersPromise.get() + |> filter { $0 != nil } + |> take(1) + |> mapToSignal { peers -> Signal in + return account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(memberId) + } + |> deliverOnMainQueue + |> mapToSignal { peer -> Signal in + if let peer = peer, let peers = peers { + var updatedPeers = peers + var found = false + for i in 0 ..< updatedPeers.count { + if updatedPeers[i].peer.id == memberId { + found = true + break + } + } + if !found { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + updatedPeers.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: peer.id, invitedAt: timestamp), peer: peer)) + peersPromise.set(.single(updatedPeers)) + } + } + return .complete() + } + } + |> mapError { _ -> AddPeerMemberError in return .generic } + + return addPeerMember(account: account, peerId: peerId, memberId: memberId) + |> then(applyMembers) + } else { + return .complete() + } + } + presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + addMembersDisposable.set(addMember.start()) + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + updateState { state in + if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { + return state.withUpdatedPeerIdWithRevealedOptions(peerId) + } else { + return state + } + } + }, removePeer: { memberId in + updateState { + return $0.withUpdatedRemovingPeerId(memberId) + } + + let applyPeers: Signal = peersPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { peers -> Signal in + if let peers = peers { + var updatedPeers = peers + for i in 0 ..< updatedPeers.count { + if updatedPeers[i].peer.id == memberId { + updatedPeers.remove(at: i) + break + } + } + peersPromise.set(.single(updatedPeers)) + } + + return .complete() + } + + removePeerDisposable.set((removePeerMember(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + }, completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + + })) + }) + + let peerView = account.viewTracker.peerView(peerId) + + let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelMembers(account: account, peerId: peerId) |> map { Optional($0) }) + + peersPromise.set(peersSignal) + + var previousPeers: [RenderedChannelParticipant]? + + let signal = combineLatest(statePromise.get(), peerView, peersPromise.get()) + |> deliverOnMainQueue + |> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelMembersEntry.ItemGenerationArguments)) in + var rightNavigationButton: ItemListNavigationButton? + if let peers = peers, !peers.isEmpty { + if state.editing { + rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + updateState { state in + return state.withUpdatedEditing(false) + } + }) + } else { + rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + updateState { state in + return state.withUpdatedEditing(true) + } + }) + } + } + + var emptyStateItem: ItemListControllerEmptyStateItem? + if peers == nil { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + } + + let previous = previousPeers + previousPeers = peers + + let controllerState = ItemListControllerState(title: "Members", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let listState = ItemListNodeState(entries: ChannelMembersControllerEntries(account: account, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window, with: p) + } + } + return controller +} diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 9c7278c308..3fe4fc8eee 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -204,7 +204,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return ItemListActivityTextItem(displayActivity: false, text: NSAttributedString(string: "Sorry, you have reserved too many public usernames. You can revoke the link from one of your older groups or channels, or create a private entity instead.", textColor: UIColor(0xcf3030)), sectionId: self.section) } case let .privateLink(link): - return ItemListActionItem(title: link ?? "Loading", kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(title: link ?? "Loading...", kind: link != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { if let link = link { arguments.displayPrivateLinkMenu(link) } @@ -257,7 +257,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { if let addressName = peer.addressName { label = "t.me/" + addressName } - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .text(label), label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .text(label), label: nil, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.revokePeerId(peerId) @@ -432,7 +432,11 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo } 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.")) + if isGroup { + entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time.")) + } else { + entries.append(.publicLinkInfo("People can join your channel by following this link. You can revoke the link at any time.")) + } } } @@ -485,7 +489,12 @@ private func updatedAddressName(state: ChannelVisibilityControllerState, peer: T } } -public func channelVisibilityController(account: Account, peerId: PeerId) -> ViewController { +public enum ChannelVisibilityControllerMode { + case initialSetup + case generic +} + +public func channelVisibilityController(account: Account, peerId: PeerId, mode: ChannelVisibilityControllerMode) -> ViewController { let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelVisibilityControllerState()) let updateState: ((ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void = { f in @@ -503,6 +512,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie })) var dismissImpl: (() -> Void)? + var nextImpl: (() -> Void)? var displayPrivateLinkMenuImpl: ((String) -> Void)? let actionsDisposable = DisposableSet() @@ -516,6 +526,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie let revokeAddressNameDisposable = MetaDisposable() actionsDisposable.add(revokeAddressNameDisposable) + actionsDisposable.add(ensuredExistingPeerExportedInvitation(account: account, peerId: peerId).start()) + let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in updateState { state in return state.withUpdatedSelectedType(type) @@ -571,8 +583,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie }) let peerView = account.viewTracker.peerView(peerId) + |> deliverOnMainQueue - let signal = combineLatest(statePromise.get(), peerView, peersDisablingAddressNameAssignment.get()) + let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue) |> map { state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, ChannelVisibilityEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) @@ -595,7 +608,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie } } - rightNavigationButton = ItemListNavigationButton(title: "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: mode == .initialSetup ? "Next" : "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(state: state, peer: peer) @@ -618,10 +631,20 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie return state.withUpdatedUpdatingAddressName(false) } - dismissImpl?() + switch mode { + case .initialSetup: + nextImpl?() + case .generic: + dismissImpl?() + } })) } else { - dismissImpl?() + switch mode { + case .initialSetup: + nextImpl?() + case .generic: + dismissImpl?() + } } }) } @@ -633,9 +656,15 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie } } - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { - dismissImpl?() - }) + let leftNavigationButton: ItemListNavigationButton? + switch mode { + case .initialSetup: + leftNavigationButton = nil + case .generic: + 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, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) @@ -646,9 +675,15 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie } let controller = ItemListController(signal) + controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) dismissImpl = { [weak controller] in controller?.dismiss() } + nextImpl = { [weak controller] in + if let controller = controller { + (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: account, peerId: peerId), animated: true) + } + } displayPrivateLinkMenuImpl = { [weak controller] text in if let strongController = controller { var resultItemNode: ListViewItemNode? diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index a835cce4b1..b4651d4862 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -47,6 +47,7 @@ public class ChatController: TelegramController { private let editingMessage = ValuePromise(false, ignoreRepeated: true) private let startingBot = ValuePromise(false, ignoreRepeated: true) + private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) private let botCallbackAlertMessage = Promise(nil) private var botCallbackAlertMessageDisposable: Disposable? @@ -54,6 +55,7 @@ public class ChatController: TelegramController { private var resolveUrlDisposable: MetaDisposable? private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? + private var urlPreviewQueryState: (URL?, Disposable)? private var audioRecorderValue: ManagedAudioRecorder? private var audioRecorderFeedback: HapticFeedback? @@ -61,12 +63,18 @@ public class ChatController: TelegramController { private var audioRecorderDisposable: Disposable? private var buttonKeyboardMessageDisposable: Disposable? + private var cachedDataDisposable: Disposable? private var chatUnreadCountDisposable: Disposable? private var peerInputActivitiesDisposable: Disposable? private var recentlyUsedInlineBotsValue: [Peer] = [] private var recentlyUsedInlineBotsDisposable: Disposable? + private var unpinMessageDisposable: MetaDisposable? + + private let typingActivityPromise = Promise() + private var typingActivityDisposable: Disposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) { self.account = account self.peerId = peerId @@ -201,7 +209,7 @@ public class ChatController: TelegramController { if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) { - strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in + strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in if let node = node { return (node, frame) } else { @@ -212,34 +220,12 @@ public class ChatController: TelegramController { } } }, navigateToMessage: { [weak self] fromId, id in - if let strongSelf = self, strongSelf.isNodeLoaded { - if id.peerId == strongSelf.peerId { - var fromIndex: MessageIndex? - - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { - fromIndex = MessageIndex(message) - } - - if let fromIndex = fromIndex { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message)) - } else { - strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in - if let strongSelf = strongSelf, let index = index { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index) - } - })) - } - } - } else { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id.peerId, messageId: id)) - } - } + self?.navigateToMessage(from: fromId, to: id) }, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessageSelection: { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { + if let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } }) } } @@ -257,7 +243,7 @@ public class ChatController: TelegramController { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() } }, sendSticker: { [weak self] file in if let strongSelf = self { @@ -284,7 +270,7 @@ public class ChatController: TelegramController { }) { var updatedContexts = $0 updatedContexts.append(.requestInProgress) - return updatedContexts + return updatedContexts.sorted() } return $0 } @@ -359,14 +345,14 @@ public class ChatController: TelegramController { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]).start() } }, openInstantPage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let instantPage = content.instantPage { + if let _ = content.instantPage { let pageController = InstantPageController(account: strongSelf.account, webPage: webpage) (strongSelf.navigationController as? NavigationController)?.pushViewController(pageController) } @@ -411,8 +397,8 @@ public class ChatController: TelegramController { return updatedContexts } else { var updatedContexts = $0 - updatedContexts.insert(.chatInfo, at: 0) - return updatedContexts + updatedContexts.append(.chatInfo) + return updatedContexts.sorted() } } }) @@ -473,7 +459,7 @@ public class ChatController: TelegramController { } else { var updatedContexts = $0 updatedContexts.append(.toastAlert(message)) - return updatedContexts + return updatedContexts.sorted() } } else { if let index = $0.index(where: { @@ -501,22 +487,6 @@ public class ChatController: TelegramController { if strongSelf.audioRecorderValue !== audioRecorder { strongSelf.audioRecorderValue = audioRecorder - if let audioRecorder = audioRecorder { - /*(audioRecorder.recordingState - |> filter { state in - switch state { - case .recording: - return true - case .paused: - return false - } - } |> take(1) |> deliverOnMainQueue).start(completed: { - self?.audioRecorderFeedback?.tap() - })*/ - } else { - strongSelf.audioRecorderFeedback = nil - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in if let audioRecorder = audioRecorder { @@ -540,6 +510,13 @@ public class ChatController: TelegramController { if let botStart = botStart, case .automatic = botStart.behavior { self.startBot(botStart.payload) } + + self.typingActivityDisposable = (self.typingActivityPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.account.updateLocalInputActivity(peerId: strongSelf.peerId, activity: .typingText, isPresent: value) + } + }) } required public init(coder aDecoder: NSCoder) { @@ -560,12 +537,16 @@ public class ChatController: TelegramController { self.resolvePeerByNameDisposable?.dispose() self.botCallbackAlertMessageDisposable?.dispose() self.contextQueryState?.1.dispose() + self.urlPreviewQueryState?.1.dispose() self.audioRecorderDisposable?.dispose() self.buttonKeyboardMessageDisposable?.dispose() + self.cachedDataDisposable?.dispose() self.resolveUrlDisposable?.dispose() self.chatUnreadCountDisposable?.dispose() self.peerInputActivitiesDisposable?.dispose() self.recentlyUsedInlineBotsDisposable?.dispose() + self.unpinMessageDisposable?.dispose() + self.typingActivityDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -582,7 +563,52 @@ public class ChatController: TelegramController { |> beforeNext { [weak self] combinedInitialData in if let strongSelf = self, let combinedInitialData = combinedInitialData { if let interfaceState = combinedInitialData.initialData?.chatInterfaceState as? ChatInterfaceState { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) }) + var pinnedMessageId: MessageId? + var peerIsBlocked: Bool = false + var canReport: Bool = false + if let cachedData = combinedInitialData.cachedData as? CachedChannelData { + pinnedMessageId = cachedData.pinnedMessageId + canReport = cachedData.reportStatus == .canReport + } else if let cachedData = combinedInitialData.cachedData as? CachedUserData { + peerIsBlocked = cachedData.isBlocked + canReport = cachedData.reportStatus == .canReport + } else if let cachedData = combinedInitialData.cachedData as? CachedGroupData { + canReport = cachedData.reportStatus == .canReport + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage).updatedPinnedMessageId(pinnedMessageId).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in + if pinnedMessageId != nil { + if !context.contains(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.append(.pinnedMessage) + return updatedContexts.sorted() + } else { + return context + } + } else { + if let index = context.index(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context + } + } + }) + }) } } } @@ -603,6 +629,60 @@ public class ChatController: TelegramController { } }) + self.cachedDataDisposable = self.chatDisplayNode.historyNode.cachedPeerData.start(next: { [weak self] cachedData in + if let strongSelf = self { + var pinnedMessageId: MessageId? + var peerIsBlocked: Bool = false + var canReport: Bool = false + if let cachedData = cachedData as? CachedChannelData { + pinnedMessageId = cachedData.pinnedMessageId + canReport = cachedData.reportStatus == .canReport + } else if let cachedData = cachedData as? CachedUserData { + peerIsBlocked = cachedData.isBlocked + canReport = cachedData.reportStatus == .canReport + } else if let cachedData = cachedData as? CachedGroupData { + canReport = cachedData.reportStatus == .canReport + } + if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || strongSelf.presentationInterfaceState.canReportPeer != canReport { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedPinnedMessageId(pinnedMessageId).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in + if pinnedMessageId != nil { + if !context.contains(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.append(.pinnedMessage) + return updatedContexts.sorted() + } else { + return context + } + } else { + if let index = context.index(where: { + switch $0 { + case .pinnedMessage: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context + } + } + }) + }) + } + } + }) + self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().start(next: { [weak self] state in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -677,7 +757,7 @@ public class ChatController: TelegramController { stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { @@ -786,6 +866,12 @@ public class ChatController: TelegramController { } } + self.chatDisplayNode.updateTypingActivity = { [weak self] in + if let strongSelf = self { + strongSelf.typingActivityPromise.set(Signal.single(true) |> then(Signal.single(false) |> delay(4.0, queue: Queue.mainQueue()))) + } + } + self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -890,6 +976,8 @@ public class ChatController: TelegramController { if let strongSelf = self { } + }, navigateToMessage: { [weak self] messageId in + self?.navigateToMessage(from: nil, to: messageId) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) }, togglePeerNotifications: { @@ -957,7 +1045,76 @@ public class ChatController: TelegramController { }) let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() } - }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get())) + }, unblockPeer: { [weak self] in + self?.unblockPeer() + }, pinMessage: { [weak self] messageId in + if let strongSelf = self { + if let peer = strongSelf.presentationInterfaceState.peer { + if let channel = peer as? TelegramChannel { + switch channel.role { + case .creator, .moderator, .editor: + let pinAction: (Bool) -> Void = { notify in + if let strongSelf = self { + let disposable: MetaDisposable + if let current = strongSelf.unpinMessageDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.unpinMessageDisposable = disposable + } + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .pin(id: messageId, silent: !notify)).start()) + } + } + strongSelf.present(standardTextAlertController(title: nil, text: "Pin this message and notify all members of the group?", actions: [TextAlertAction(type: .genericAction, title: "Only Pin", action: { + pinAction(false) + }), TextAlertAction(type: .defaultAction, title: "Yes", action: { + pinAction(true) + })]), in: .window) + case .member: + if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) + }) + } + } + } + } + } + }, unpinMessage: { [weak self] in + if let strongSelf = self { + if let peer = strongSelf.presentationInterfaceState.peer { + if let channel = peer as? TelegramChannel { + switch channel.role { + case .creator, .moderator, .editor: + strongSelf.present(standardTextAlertController(title: nil, text: "Would you like to unpin this Message?", actions: [TextAlertAction(type: .genericAction, title: "No", action: {}), TextAlertAction(type: .genericAction, title: "Yes", action: { + if let strongSelf = self { + let disposable: MetaDisposable + if let current = strongSelf.unpinMessageDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.unpinMessageDisposable = disposable + } + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .clear).start()) + } + })]), in: .window) + case .member: + if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) + }) + } + } + } + } + } + }, reportPeer: { [weak self] in + self?.reportPeer() + }, dismissReportPeer: { [weak self] in + self?.dismissReportPeer() + }, deleteChat: { [weak self] in + self?.deleteChat() + }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get())) self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { @@ -974,7 +1131,7 @@ public class ChatController: TelegramController { }) let postbox = self.account.postbox - var previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) + let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) self.peerInputActivitiesDisposable = (self.account.peerInputActivities(peerId: peerId) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true @@ -1001,7 +1158,7 @@ public class ChatController: TelegramController { peerCache[peerId] = peer } } - previousPeerCache.swap(peerCache) + let _ = previousPeerCache.swap(peerCache) return result } } @@ -1148,6 +1305,28 @@ public class ChatController: TelegramController { } } + if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { + self.urlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + self.urlPreviewQueryState = (updatedUrlPreviewUrl, (updatedUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedUrlPreview(result($0.urlPreview)) + }) + } + } + })) + inScope = false + if let inScopeResult = inScopeResult { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(inScopeResult(updatedChatPresentationInterfaceState.urlPreview)) + } + } + self.presentationInterfaceState = updatedChatPresentationInterfaceState if self.isNodeLoaded { self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive) @@ -1201,8 +1380,11 @@ public class ChatController: TelegramController { case .clearHistory: let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak actionSheet] in + ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId).start() + } }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in @@ -1225,7 +1407,7 @@ public class ChatController: TelegramController { } private func presentMediaPicker(fileMode: Bool) { - legacyAssetPicker(fileMode: fileMode).start(next: { [weak self] generator in + let _ = legacyAssetPicker(fileMode: fileMode).start(next: { [weak self] generator in if let strongSelf = self { var presentOverlayController: ((UIViewController) -> (() -> Void))? let controller = generator({ controller in @@ -1234,7 +1416,7 @@ public class ChatController: TelegramController { let legacyController = LegacyController(legacyController: controller, presentation: .modal) presentOverlayController = { [weak legacyController] controller in - if let strongSelf = self, let legacyController = legacyController { + if let legacyController = legacyController { let childController = LegacyController(legacyController: controller, presentation: .custom) legacyController.present(childController, in: .window) return { [weak childController] in @@ -1310,7 +1492,7 @@ public class ChatController: TelegramController { if let audioRecorderValue = self.audioRecorderValue { audioRecorderValue.stop() if sendAudio { - (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in + let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, let data = data { if data.duration < 0.5 { strongSelf.audioRecorderFeedback?.error() @@ -1340,6 +1522,36 @@ public class ChatController: TelegramController { self.audioRecorder.set(.single(nil)) } + private func navigateToMessage(from fromId: MessageId?, to toId: MessageId) { + if self.isNodeLoaded { + if toId.peerId == self.peerId { + var fromIndex: MessageIndex? + + if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { + fromIndex = MessageIndex(message) + } else { + if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { + fromIndex = MessageIndex(message) + } + } + + if let fromIndex = fromIndex { + if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(toId) { + self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message)) + } else { + self.messageIndexDisposable.set((self.account.postbox.messageIndexAtId(toId) |> deliverOnMainQueue).start(next: { [weak self] index in + if let strongSelf = self, let index = index { + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index) + } + })) + } + } + } else { + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: toId.peerId, messageId: toId)) + } + } + } + private func openPeer(_ peerId: PeerId?, _ navigation: ChatControllerInteractionNavigateToPeer) { if peerId == self.peerId { switch navigation { @@ -1367,14 +1579,13 @@ public class ChatController: TelegramController { break case let .chat(textInputState): if let textInputState = textInputState { - (self.account.postbox.modify({ modifier -> Void in + let _ = (self.account.postbox.modify({ modifier -> Void in modifier.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { return ChatInterfaceState().withUpdatedComposeInputState(textInputState) } - return currentState }) })).start(completed: { [weak self] in if let strongSelf = self { @@ -1406,14 +1617,13 @@ public class ChatController: TelegramController { }) strongController.dismiss() } else { - (strongSelf.account.postbox.modify({ modifier -> Void in + let _ = (strongSelf.account.postbox.modify({ modifier -> Void in modifier.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedComposeInputState(textInputState) } else { return ChatInterfaceState().withUpdatedComposeInputState(textInputState) } - return currentState }) }) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { @@ -1442,6 +1652,36 @@ public class ChatController: TelegramController { } } + private func unblockPeer() { + let unblockingPeer = self.unblockingPeer + unblockingPeer.set(true) + self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.account, peerId: self.peerId, isBlocked: false) |> afterDisposed({ + Queue.mainQueue().async { + unblockingPeer.set(false) + } + })).start()) + } + + private func reportPeer() { + self.editMessageDisposable.set((TelegramCore.reportPeer(account: self.account, peerId: self.peerId) |> afterDisposed({ + Queue.mainQueue().async { + } + })).start()) + } + + private func dismissReportPeer() { + self.editMessageDisposable.set((TelegramCore.dismissReportPeer(account: self.account, peerId: self.peerId) |> afterDisposed({ + Queue.mainQueue().async { + } + })).start()) + } + + private func deleteChat() { + self.chatDisplayNode.historyNode.disconnect() + let _ = removePeerChat(postbox: self.account.postbox, peerId: self.peerId).start() + (self.navigationController as? NavigationController)?.popToRoot(animated: true) + } + private func startBot(_ payload: String?) { let startingBot = self.startingBot startingBot.set(true) diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 71c12e17b6..f8c043a6e7 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -43,6 +43,7 @@ class ChatControllerNode: ASDisplayNode { var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } + var updateTypingActivity: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } @@ -159,6 +160,10 @@ class ChatControllerNode: ASDisplayNode { self.textInputPanelNode?.displayAttachmentMenu = { [weak self] in self?.displayAttachmentMenu() } + + self.textInputPanelNode?.updateActivity = { [weak self] in + self?.updateTypingActivity() + } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) { @@ -178,7 +183,7 @@ class ChatControllerNode: ASDisplayNode { var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false - if let titleAccessoryPanelNode = titlePanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { + if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { if self.titleAccessoryPanelNode != titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode self.titleAccessoryPanelNode = titleAccessoryPanelNode @@ -310,6 +315,8 @@ class ChatControllerNode: ASDisplayNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedEditMessage(nil) }) + } else if let _ = accessoryPanelNode as? WebpagePreviewAccessoryPanelNode { + } } } @@ -393,14 +400,16 @@ class ChatControllerNode: ASDisplayNode { } let inputContextPanelsFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - inputPanelsHeight - insets.top - UIScreenPixel))) + let inputContextPanelsOverMainPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - (inputPanelSize == nil ? CGFloat(0.0) : inputPanelSize!.height) - insets.top - UIScreenPixel))) if let inputContextPanelNode = self.inputContextPanelNode { + let panelFrame = inputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if immediatelyLayoutInputContextPanelAndAnimateAppearance { - inputContextPanelNode.frame = inputContextPanelsFrame - inputContextPanelNode.updateLayout(size: inputContextPanelsFrame.size, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) - } else if !inputContextPanelNode.frame.equalTo(inputContextPanelsFrame) { - transition.updateFrame(node: inputContextPanelNode, frame: inputContextPanelsFrame) - inputContextPanelNode.updateLayout(size: inputContextPanelsFrame.size, transition: transition, interfaceState: self.chatPresentationInterfaceState) + inputContextPanelNode.frame = panelFrame + inputContextPanelNode.updateLayout(size: panelFrame.size, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + } else if !inputContextPanelNode.frame.equalTo(panelFrame) { + transition.updateFrame(node: inputContextPanelNode, frame: panelFrame) + inputContextPanelNode.updateLayout(size: panelFrame.size, transition: transition, interfaceState: self.chatPresentationInterfaceState) } } @@ -479,8 +488,9 @@ class ChatControllerNode: ASDisplayNode { dismissedInputContextPanelNode.removeFromSupernode() } } - if !dismissedInputContextPanelNode.frame.equalTo(inputContextPanelsFrame) { - transition.updateFrame(node: dismissedInputContextPanelNode, frame: inputContextPanelsFrame, completion: { _ in + let panelFrame = dismissedInputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame + if !dismissedInputContextPanelNode.frame.equalTo(panelFrame) { + transition.updateFrame(node: dismissedInputContextPanelNode, frame: panelFrame, completion: { _ in frameCompleted = true completed() }) diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index 0c4945b962..d8dc3dfe17 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -98,7 +98,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { if let fileName = file.fileName { pathExtension = (fileName as NSString).pathExtension } - let data = account.postbox.mediaBox.resourceData(file.resource, pathExtension: pathExtension, complete: true) + let data = account.postbox.mediaBox.resourceData(file.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false)) |> deliverOnMainQueue self.dataDisposable.set(data.start(next: { [weak self] data in if let strongSelf = self { diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index d466f99bf1..39da22008c 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -179,7 +179,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeChatInfoEntry: false)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -329,4 +329,8 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { self.dequeueHistoryViewTransition() } } + + public func disconnect() { + self.historyDisposable.set(nil) + } } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index db614a6fe6..29263b0e15 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -23,6 +23,7 @@ enum ChatHistoryViewUpdateType { public struct ChatHistoryCombinedInitialData { let initialData: InitialMessageHistoryData? let buttonKeyboardMessage: Message? + let cachedData: CachedPeerData? } enum ChatHistoryViewUpdate { @@ -66,6 +67,7 @@ struct ChatHistoryViewTransition { let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? + let cachedData: CachedPeerData? } struct ChatHistoryListViewTransition { @@ -78,6 +80,7 @@ struct ChatHistoryListViewTransition { let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? + let cachedData: CachedPeerData? } private func maxIncomingMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageIndex? { @@ -148,7 +151,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage) + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData) } private final class ChatHistoryTransactionOpaqueState { @@ -186,6 +189,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return self._initialData.get() } + private let _cachedPeerData = Promise() + public var cachedPeerData: Signal { + return self._cachedPeerData.get() + } + private var _buttonKeyboardMessage = Promise(nil) private var currentButtonKeyboardMessage: Message? public var buttonKeyboardMessage: Signal { @@ -228,9 +236,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let fixedCombinedReadState = Atomic(value: nil) var additionalData: [AdditionalMessageHistoryViewData] = [] - if peerId.namespace == Namespaces.Peer.CloudUser { - additionalData.append(.cachedPeerData(peerId)) - } + additionalData.append(.cachedPeerData(peerId)) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged @@ -251,7 +257,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(data): - let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil) initialData = combinedInitialData Queue.mainQueue().async { [weak self] in if let strongSelf = self { @@ -260,6 +266,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf._initialData.set(.single(combinedInitialData)) } + strongSelf._cachedPeerData.set(.single(nil)) + let historyState: ChatHistoryNodeHistoryState = .loading if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -292,7 +300,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeChatInfoEntry: true)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -372,9 +380,29 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)) } + public func anchorMessageInCurrentHistoryView() -> Message? { + if let historyView = self.historyView { + if let visibleRange = self.displayedItemRange.visibleRange { + var index = 0 + for entry in historyView.filteredEntries.reversed() { + if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { + if case let .MessageEntry(message, _) = entry { + return message + } + } + index += 1 + } + } + + for case let .MessageEntry(message, _) in historyView.filteredEntries { + return message + } + } + return nil + } + public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - var galleryMedia: Media? for case let .MessageEntry(message, _) in historyView.filteredEntries where message.id == id { return message } @@ -402,8 +430,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } else { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData))) } + strongSelf._cachedPeerData.set(.single(transition.cachedData)) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -437,8 +466,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData))) } + strongSelf._cachedPeerData.set(.single(transition.cachedData)) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -481,4 +511,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.dequeueHistoryViewTransition() } } + + public func disconnect() { + self.historyDisposable.set(nil) + } } diff --git a/TelegramUI/ChatHistoryNode.swift b/TelegramUI/ChatHistoryNode.swift index 702798965e..f5f4908bfc 100644 --- a/TelegramUI/ChatHistoryNode.swift +++ b/TelegramUI/ChatHistoryNode.swift @@ -32,4 +32,5 @@ public protocol ChatHistoryNode: class { func messageInCurrentHistoryView(_ id: MessageId) -> Message? func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func forEachItemNode(_ f: (ASDisplayNode) -> Void) + func disconnect() } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 3634f394ea..aa10573b20 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -16,8 +16,15 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, additionalData: additionalData) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + var cachedData: CachedPeerData? + for data in view.additionalData { + if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { + cachedData = value + break + } + } if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } else { var scrollPosition: ChatHistoryViewScrollPosition? @@ -58,15 +65,22 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } } case let .InitialSearch(messageId, count): var preloaded = false var fadeIn = false return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + var cachedData: CachedPeerData? + for data in view.additionalData { + if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { + cachedData = value + break + } + } if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } else { let anchorIndex = view.anchorIndex @@ -90,12 +104,20 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun preloaded = true //case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } } case let .Navigation(index, anchorIndex): var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + var cachedData: CachedPeerData? + for data in view.additionalData { + if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { + cachedData = value + break + } + } + let genericType: ViewUpdateType if first { first = false @@ -103,13 +125,21 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + var cachedData: CachedPeerData? + for data in view.additionalData { + if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { + cachedData = value + break + } + } + let genericType: ViewUpdateType let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil if first { @@ -118,7 +148,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } } } diff --git a/TelegramUI/ChatInputContextPanelNode.swift b/TelegramUI/ChatInputContextPanelNode.swift index 62d06a5d2b..a9e421ee82 100644 --- a/TelegramUI/ChatInputContextPanelNode.swift +++ b/TelegramUI/ChatInputContextPanelNode.swift @@ -3,9 +3,15 @@ import AsyncDisplayKit import Display import TelegramCore +enum ChatInputContextPanelPlacement { + case overPanels + case overTextInput +} + class ChatInputContextPanelNode: ASDisplayNode { let account: Account var interfaceInteraction: ChatPanelInterfaceInteraction? + var placement: ChatInputContextPanelPlacement = .overPanels init(account: Account) { self.account = account diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index f0385ff75a..c854050dbc 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -188,3 +188,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } } + +func urlPreviewForPresentationInterfaceState() { + +} diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index 14b48c30af..e2b2ac6eba 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -128,19 +128,22 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { struct ChatInterfaceMessageActionsState: Coding, Equatable { let closedButtonKeyboardMessageId: MessageId? let processedSetupReplyMessageId: MessageId? + let closedPinnedMessageId: MessageId? var isEmpty: Bool { - return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil + return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil && self.closedPinnedMessageId == nil } init() { self.closedButtonKeyboardMessageId = nil self.processedSetupReplyMessageId = nil + self.closedPinnedMessageId = nil } - init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?) { + init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?, closedPinnedMessageId: MessageId?) { self.closedButtonKeyboardMessageId = closedButtonKeyboardMessageId self.processedSetupReplyMessageId = processedSetupReplyMessageId + self.closedPinnedMessageId = closedPinnedMessageId } init(decoder: Decoder) { @@ -155,6 +158,12 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { } else { self.processedSetupReplyMessageId = nil } + + if let closedPinnedMessageIdPeerId = (decoder.decodeInt64ForKey("cp.p") as Int64?), let closedPinnedMessageIdNamespace = (decoder.decodeInt32ForKey("cp.n") as Int32?), let closedPinnedMessageIdId = (decoder.decodeInt32ForKey("cp.i") as Int32?) { + self.closedPinnedMessageId = MessageId(peerId: PeerId(closedPinnedMessageIdPeerId), namespace: closedPinnedMessageIdNamespace, id: closedPinnedMessageIdId) + } else { + self.closedPinnedMessageId = nil + } } func encode(_ encoder: Encoder) { @@ -177,18 +186,32 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { encoder.encodeNil(forKey: "pb.n") encoder.encodeNil(forKey: "pb.i") } + + if let closedPinnedMessageId = self.closedPinnedMessageId { + encoder.encodeInt64(closedPinnedMessageId.peerId.toInt64(), forKey: "cp.p") + encoder.encodeInt32(closedPinnedMessageId.namespace, forKey: "cp.n") + encoder.encodeInt32(closedPinnedMessageId.id, forKey: "cp.i") + } else { + encoder.encodeNil(forKey: "cp.p") + encoder.encodeNil(forKey: "cp.n") + encoder.encodeNil(forKey: "cp.i") + } } static func ==(lhs: ChatInterfaceMessageActionsState, rhs: ChatInterfaceMessageActionsState) -> Bool { - return lhs.closedButtonKeyboardMessageId == rhs.closedButtonKeyboardMessageId && lhs.processedSetupReplyMessageId == rhs.processedSetupReplyMessageId + return lhs.closedButtonKeyboardMessageId == rhs.closedButtonKeyboardMessageId && lhs.processedSetupReplyMessageId == rhs.processedSetupReplyMessageId && lhs.closedPinnedMessageId == rhs.closedPinnedMessageId } func withUpdatedClosedButtonKeyboardMessageId(_ closedButtonKeyboardMessageId: MessageId?) -> ChatInterfaceMessageActionsState { - return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: closedButtonKeyboardMessageId, processedSetupReplyMessageId: self.processedSetupReplyMessageId) + return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: closedButtonKeyboardMessageId, processedSetupReplyMessageId: self.processedSetupReplyMessageId, closedPinnedMessageId: self.closedPinnedMessageId) } func withUpdatedProcessedSetupReplyMessageId(_ processedSetupReplyMessageId: MessageId?) -> ChatInterfaceMessageActionsState { - return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: self.closedButtonKeyboardMessageId, processedSetupReplyMessageId: processedSetupReplyMessageId) + return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: self.closedButtonKeyboardMessageId, processedSetupReplyMessageId: processedSetupReplyMessageId, closedPinnedMessageId: self.closedPinnedMessageId) + } + + func withUpdatedClosedPinnedMessageId(_ closedPinnedMessageId: MessageId?) -> ChatInterfaceMessageActionsState { + return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: self.closedButtonKeyboardMessageId, processedSetupReplyMessageId: self.processedSetupReplyMessageId, closedPinnedMessageId: closedPinnedMessageId) } } @@ -334,7 +357,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { - var updatedComposeInputState = inputState + let updatedComposeInputState = inputState return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index ee535cfb39..d992dd3bcb 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -34,6 +34,15 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } + } else if let urlPreview = chatPresentationInterfaceState.urlPreview { + if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode, previewPanelNode.webpage.id == urlPreview.id { + previewPanelNode.interfaceInteraction = interfaceInteraction + return previewPanelNode + } else { + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } } else { return nil } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index eac30c1e04..5165bbc587 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -12,12 +12,23 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat var actions: [ContextMenuAction] = [] var canReply = false - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - switch channel.role { - case .creator, .editor, .moderator: - canReply = true - case .member: - canReply = false + var canPin = false + if let channel = peer as? TelegramChannel { + switch channel.info { + case .broadcast: + switch channel.role { + case .creator, .editor, .moderator: + canReply = true + case .member: + canReply = false + } + case .group: + switch channel.role { + case .creator, .editor, .moderator: + canPin = true + case .member: + canPin = false + } } } else { canReply = true @@ -59,6 +70,18 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat } })) + if canPin { + if chatPresentationInterfaceState.pinnedMessageId != message.id { + actions.append(ContextMenuAction(content: .text("Pin"), action: { + interfaceInteraction.pinMessage(message.id) + })) + } else { + actions.append(ContextMenuAction(content: .text("Unpin"), action: { + interfaceInteraction.unpinMessage() + })) + } + } + actions.append(ContextMenuAction(content: .text("More..."), action: { interfaceInteraction.beginMessageSelection(message.id) })) diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index 107af2c452..e610038b0e 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -179,3 +179,34 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation return (nil, .single({ _ in return nil })) } } + +private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) + +func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: URL?) -> (URL?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { + if let dataDetector = dataDetector { + let text = chatPresentationInterfaceState.interfaceState.composeInputState.inputText + let utf16 = text.utf16 + + var detectedUrl: URL? + + let matches = dataDetector.matches(in: text, options: [], range: NSRange(location: 0, length: utf16.count)) + if let match = matches.first { + let urlText = (text as NSString).substring(with: match.range) + detectedUrl = URL(string: urlText) + } + + if detectedUrl != currentQuery { + if let detectedUrl = detectedUrl { + return (detectedUrl, webpagePreview(account: account, url: detectedUrl.absoluteString) |> map { value in + return { _ in return value } + }) + } else { + return (nil, .single({ _ in return nil })) + } + } else { + return nil + } + } else { + return (nil, .single({ _ in return nil })) + } +} diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 9871cbacf1..4c2f8d3b77 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -16,8 +16,56 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState return panel } } else { + if chatPresentationInterfaceState.peerIsBlocked { + if let currentPanel = currentPanel as? ChatUnblockInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + return currentPanel + } else { + let panel = ChatUnblockInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + if let peer = chatPresentationInterfaceState.peer { - if let channel = peer as? TelegramChannel { + if let secretChat = peer as? TelegramSecretChat { + switch secretChat.embeddedState { + case .handshake: + if let currentPanel = currentPanel as? SecretChatHandshakeStatusInputPanelNode { + return currentPanel + } else { + let panel = SecretChatHandshakeStatusInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .terminated: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .active: + break + } + } else if let channel = peer as? TelegramChannel { + switch channel.participationStatus { + case .kicked, .left: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .member: + break + } switch channel.info { case .broadcast: switch channel.role { @@ -46,10 +94,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState break } } + } else if let group = peer as? TelegramGroup { + switch group.membership { + case .Removed, .Left: + if let currentPanel = currentPanel as? DeleteChatInputPanelNode { + return currentPanel + } else { + let panel = DeleteChatInputPanelNode() + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .Member: + break + } } var displayBotStartPanel = false - if let botStartPayload = chatPresentationInterfaceState.botStartPayload { + if let _ = chatPresentationInterfaceState.botStartPayload { displayBotStartPanel = true } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { if let user = chatPresentationInterfaceState.peer as? TelegramUser, user.botInfo != nil { diff --git a/TelegramUI/ChatInterfaceTitlePanelNodes.swift b/TelegramUI/ChatInterfaceTitlePanelNodes.swift index 5994d0545e..70118bd1b9 100644 --- a/TelegramUI/ChatInterfaceTitlePanelNodes.swift +++ b/TelegramUI/ChatInterfaceTitlePanelNodes.swift @@ -1,9 +1,43 @@ import Foundation import TelegramCore -func titlePanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatTitleAccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatTitleAccessoryPanelNode? { +func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatTitleAccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatTitleAccessoryPanelNode? { + var selectedContext: ChatTitlePanelContext? if !chatPresentationInterfaceState.titlePanelContexts.isEmpty { - switch chatPresentationInterfaceState.titlePanelContexts[chatPresentationInterfaceState.titlePanelContexts.count - 1] { + loop: for context in chatPresentationInterfaceState.titlePanelContexts.reversed() { + switch context { + case .pinnedMessage: + if chatPresentationInterfaceState.pinnedMessageId != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId { + selectedContext = context + break loop + } + case .chatInfo, .requestInProgress, .toastAlert: + selectedContext = context + break loop + } + } + } + + if chatPresentationInterfaceState.canReportPeer && (selectedContext == nil || selectedContext! <= .pinnedMessage) { + if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode { + return currentPanel + } else { + let panel = ChatReportPeerTitlePanelNode() + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + + if let selectedContext = selectedContext { + switch selectedContext { + case .pinnedMessage: + if let currentPanel = currentPanel as? ChatPinnedMessageTitlePanelNode { + return currentPanel + } else { + let panel = ChatPinnedMessageTitlePanelNode(account: account) + panel.interfaceInteraction = interfaceInteraction + return panel + } case .chatInfo: if let currentPanel = currentPanel as? ChatInfoTitlePanelNode { return currentPanel @@ -32,5 +66,6 @@ func titlePanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } + return nil } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 754f93d0ed..3bfc811e30 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -212,7 +212,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private let highlightedBackgroundNode: ASDisplayNode let avatarNode: AvatarNode - let contentNode: ASDisplayNode let titleNode: TextNode let textNode: TextNode let dateNode: TextNode @@ -247,15 +246,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - self.contentNode = ASDisplayNode() - self.contentNode.isLayerBacked = true - self.contentNode.displaysAsynchronously = true - self.contentNode.shouldRasterizeDescendants = true - self.contentNode.isOpaque = true - self.contentNode.backgroundColor = UIColor.white - self.contentNode.contentMode = .left - self.contentNode.contentsScale = UIScreenScale - self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = true @@ -296,15 +286,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) - self.addSubnode(self.contentNode) - self.contentNode.addSubnode(self.titleNode) - self.contentNode.addSubnode(self.textNode) - self.contentNode.addSubnode(self.dateNode) - self.contentNode.addSubnode(self.statusNode) - self.contentNode.addSubnode(self.badgeBackgroundNode) - self.contentNode.addSubnode(self.badgeTextNode) - self.contentNode.addSubnode(self.mutedIconNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.dateNode) + self.addSubnode(self.statusNode) + self.addSubnode(self.badgeBackgroundNode) + self.addSubnode(self.badgeTextNode) + self.addSubnode(self.mutedIconNode) } func setupItem(item: ChatListItem) { @@ -339,9 +328,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { super.setHighlighted(highlighted, animated: animated) if highlighted { - self.contentNode.displaysAsynchronously = false - self.contentNode.backgroundColor = UIColor.clear - self.contentNode.isOpaque = false + /*var nodes: [ASDisplayNode] = [self.titleNode, self.textNode, self.dateNode, self.statusNode] + for node in nodes { + node.backgroundColor = .clear + node.recursivelyEnsureDisplaySynchronously(true) + }*/ self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { @@ -354,32 +345,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { if completed { strongSelf.highlightedBackgroundNode.removeFromSupernode() - if let item = strongSelf.layoutParams?.0, item.index.pinningIndex != nil { + /*if let item = strongSelf.layoutParams?.0, item.index.pinningIndex != nil { strongSelf.contentNode.backgroundColor = pinnedBackgroundColor } else { strongSelf.contentNode.backgroundColor = UIColor.white } - strongSelf.contentNode.isOpaque = true - strongSelf.contentNode.displaysAsynchronously = true + if !strongSelf.contentNode.isOpaque { + strongSelf.contentNode.isOpaque = true + strongSelf.contentNode.recursivelyEnsureDisplaySynchronously(true) + }*/ } } }) self.highlightedBackgroundNode.alpha = 0.0 } else { self.highlightedBackgroundNode.removeFromSupernode() - if let item = self.layoutParams?.0, item.index.pinningIndex != nil { + + /*if let item = self.layoutParams?.0, item.index.pinningIndex != nil { self.contentNode.backgroundColor = pinnedBackgroundColor } else { self.contentNode.backgroundColor = UIColor.white } self.contentNode.isOpaque = true - self.contentNode.displaysAsynchronously = true + self.contentNode.displaysAsynchronously = true*/ } } } } + func asyncLayout() -> (_ item: ChatListItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNode.asyncLayout(self.textNode) @@ -526,9 +521,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { muteWidth = currentMutedIconImage.size.width + 4.0 } - let contentRect = CGRect(origin: CGPoint(x: 2.0, y: 12.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0 - editingOffset, height: 68.0 - 12.0 - 9.0)) + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 12.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0 - editingOffset, height: 68.0 - 12.0 - 9.0)) - let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: contentRect.width, height: CGFloat.greatestFiniteMagnitude), nil) + let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), nil) let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), nil) @@ -539,9 +534,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { badgeSize = 0.0 } - let (textLayout, textApply) = textLayout(textAttributedString, nil, 1, .end, CGSize(width: contentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), nil) + let (textLayout, textApply) = textLayout(textAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), nil) - let titleRect = CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width - dateLayout.size.width - 10.0 - statusWidth - muteWidth, height: contentRect.height)) + let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - muteWidth, height: rawContentRect.height)) let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), nil) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) @@ -593,21 +588,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: editingOffset + 10.0 + revealOffset, y: 4.0), size: CGSize(width: 60.0, height: 60.0))) - let previousContentNodeFrame = strongSelf.contentNode.frame - transition.updateFrame(node: strongSelf.contentNode, frame: CGRect(origin: CGPoint(x: editingOffset + 78.0 + revealOffset, y: 0.0), size: CGSize(width: width - 78.0, height: 60.0))) let _ = dateApply() let _ = textApply() let _ = titleApply() let _ = badgeApply() - strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) + let contentRect = rawContentRect.offsetBy(dx: editingOffset + 78.0 + revealOffset, dy: 0.0) + + transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)) if let statusImage = statusImage { strongSelf.statusNode.image = statusImage strongSelf.statusNode.isHidden = false let statusSize = statusImage.size - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize)) } else { strongSelf.statusNode.image = nil strongSelf.statusNode.isHidden = true @@ -621,38 +616,31 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeLayout.size) - strongSelf.badgeTextNode.frame = badgeTextFrame - strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame + transition.updateFrame(node: strongSelf.badgeTextNode, frame: badgeTextFrame) + transition.updateFrame(node: strongSelf.badgeBackgroundNode, frame: badgeBackgroundFrame) } else { strongSelf.badgeBackgroundNode.image = nil strongSelf.badgeBackgroundNode.isHidden = true } - var updateContentNode = false if let currentMutedIconImage = currentMutedIconImage { strongSelf.mutedIconNode.image = currentMutedIconImage - if strongSelf.mutedIconNode.isHidden { - updateContentNode = true - } strongSelf.mutedIconNode.isHidden = false - strongSelf.mutedIconNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleLayout.size.width + 3.0, y: contentRect.origin.y + 6.0), size: currentMutedIconImage.size) + transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleLayout.size.width + 3.0, y: contentRect.origin.y + 6.0), size: currentMutedIconImage.size)) } else { - if !strongSelf.mutedIconNode.isHidden { - updateContentNode = true - } strongSelf.mutedIconNode.image = nil strongSelf.mutedIconNode.isHidden = true } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size)) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size) + transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size)) let separatorInset: CGFloat if !nextIsPinned && item.index.pinningIndex != nil { separatorInset = 0.0 } else { - separatorInset = editingOffset + 78.0 + contentRect.origin.x + separatorInset = editingOffset + 78.0 + rawContentRect.origin.x } transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: width - separatorInset, height: separatorHeight))) @@ -660,21 +648,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) if item.index.pinningIndex != nil { strongSelf.backgroundNode.backgroundColor = pinnedBackgroundColor - if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(pinnedBackgroundColor) { + /*if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(pinnedBackgroundColor) { strongSelf.contentNode.backgroundColor = pinnedBackgroundColor updateContentNode = true - } + }*/ } else { strongSelf.backgroundNode.backgroundColor = UIColor.white - if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(UIColor.white) { + /*if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(UIColor.white) { strongSelf.contentNode.backgroundColor = UIColor.white updateContentNode = true - } + }*/ } let topNegativeInset: CGFloat = 0.0 strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) - if crossfadeContent && animated { + /*if crossfadeContent && animated { if let contents = strongSelf.contentNode.contents { let tempNode = ASDisplayNode() tempNode.isLayerBacked = true @@ -689,7 +677,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } if updateContentNode { strongSelf.contentNode.setNeedsDisplay() - } + }*/ strongSelf.setRevealOptions(peerRevealOptions) strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) @@ -728,13 +716,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 12.0), size: CGSize(width: self.contentSize.width - 78.0 - 10.0 - 1.0 - editingOffset, height: 68.0 - 12.0 - 9.0)) + + let contentRect = rawContentRect.offsetBy(dx: editingOffset + 78.0 + offset, dy: 0.0) + var avatarFrame = self.avatarNode.frame avatarFrame.origin.x = editingOffset + 10.0 + offset transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - var contentFrame = self.contentNode.frame - contentFrame.origin.x = editingOffset + 78.0 + offset - transition.updateFrame(node: self.contentNode, frame: contentFrame) + let titleFrame = self.titleNode.frame + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: titleFrame.origin.y), size: titleFrame.size)) + + let textFrame = self.textNode.frame + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: textFrame.origin.y), size: textFrame.size)) + + let dateFrame = self.dateNode.frame + transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: contentRect.origin.y + 2.0), size: dateFrame.size)) + + let statusFrame = self.statusNode.frame + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: contentRect.origin.y + 5.0), size: statusFrame.size)) + + let mutedIconFrame = self.mutedIconNode.frame + transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleFrame.size.width + 3.0, y: contentRect.origin.y + 6.0), size: mutedIconFrame.size)) + + + let badgeBackgroundFrame = self.badgeBackgroundNode.frame + let updatedBadgeBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeBackgroundFrame.size.width, y: contentRect.maxY - badgeBackgroundFrame.size.height - 2.0), size: badgeBackgroundFrame.size) + transition.updateFrame(node: self.badgeBackgroundNode, frame: updatedBadgeBackgroundFrame) + + let badgeTextFrame = self.badgeTextNode.frame + transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeBackgroundFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeTextFrame.size)) } } diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 367360341a..c4d6d5c63f 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -23,7 +23,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { self.addSubnode(self.sectionHeaderNode) self.addSubnode(self.listView) - self.disposable.set((recentPeers(account: account) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peers in + self.disposable.set((recentPeers(account: account) |> filter { !$0.isEmpty } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self { var items: [ListViewItem] = [] for peer in peers { diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index ce0e594c0f..87a8796c00 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -828,7 +828,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: false) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height)) + selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in @@ -837,7 +837,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } }) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height)) + selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.addSubnode(selectionNode) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index c8be53f941..d7d1d3e247 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -79,17 +79,23 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let resourceStatus = self.resourceStatus { switch resourceStatus { case let .fetchStatus(fetchStatus): - switch fetchStatus { - case .Fetching: - if let cancel = self.fetchControls.with({ return $0?.cancel }) { - cancel() - } - case .Remote: - if let fetch = self.fetchControls.with({ return $0?.fetch }) { - fetch() - } - case .Local: - self.activateLocalContent() + if let account = self.account, let message = self.message, message.flags.isSending { + let _ = account.postbox.modify({ modifier -> Void in + modifier.deleteMessages([message.id]) + }).start() + } else { + switch fetchStatus { + case .Fetching: + if let cancel = self.fetchControls.with({ return $0?.cancel }) { + cancel() + } + case .Remote: + if let fetch = self.fetchControls.with({ return $0?.fetch }) { + fetch() + } + case .Local: + self.activateLocalContent() + } } case .playbackStatus: if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext { diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 7696e0997a..eec990ecca 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -55,7 +55,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { switch fetchStatus { case .Fetching: if let account = self.account, let (messageId, flags) = self.messageIdAndFlags, flags.isSending { - account.postbox.modify({ modifier -> Void in + let _ = account.postbox.modify({ modifier -> Void in modifier.deleteMessages([messageId]) }).start() } @@ -94,7 +94,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { return { account, message, media, corners, automaticDownload, constrainedSize, layoutConstants in var nativeSize: CGSize - var isSecretMedia = message.containsSecretMedia + let isSecretMedia = message.containsSecretMedia var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { for attribute in message.attributes { diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index c31640c652..5eeda1fa1d 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -192,8 +192,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { Queue.mainQueue().async { node.setupItem(self) - node.updateSelectionState(animated: false) - let nodeLayout = node.asyncLayout() async { @@ -203,6 +201,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { Queue.mainQueue().async { completion(layout, { apply(animation) + node.updateSelectionState(animated: false) }) } } diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index 03b21c1033..25cc300068 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -22,7 +22,6 @@ class ChatMessageReplyInfoNode: ASTransformLayerNode { self.contentNode = ASDisplayNode() self.contentNode.displaysAsynchronously = true self.contentNode.isLayerBacked = true - self.contentNode.shouldRasterizeDescendants = true self.contentNode.contentMode = .left self.contentNode.contentsScale = UIScreenScale diff --git a/TelegramUI/ChatMessageSelectionNode.swift b/TelegramUI/ChatMessageSelectionNode.swift index c8cdbc04c6..e13d4ba9fa 100644 --- a/TelegramUI/ChatMessageSelectionNode.swift +++ b/TelegramUI/ChatMessageSelectionNode.swift @@ -21,6 +21,8 @@ final class ChatMessageSelectionNode: ASDisplayNode { self.checkNode.image = uncheckedImage self.addSubnode(self.checkNode) + + self.hitTestSlop = UIEdgeInsetsMake(0.0, 42.0, 0.0, 0.0) } override func didLoad() { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 0e70bc771d..fb718743b4 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -6,10 +6,12 @@ import TelegramCore final class ChatPanelInterfaceInteractionStatuses { let editingMessage: Signal let startingBot: Signal + let unblockingPeer: Signal - init(editingMessage: Signal, startingBot: Signal) { + init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal) { self.editingMessage = editingMessage self.startingBot = startingBot + self.unblockingPeer = unblockingPeer } } @@ -23,6 +25,7 @@ final class ChatPanelInterfaceInteraction { let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void let editMessage: (MessageId, String) -> Void let beginMessageSearch: () -> Void + let navigateToMessage: (MessageId) -> Void let openPeerInfo: () -> Void let togglePeerNotifications: () -> Void let sendContextResult: (ChatContextResultCollection, ChatContextResult) -> Void @@ -33,9 +36,15 @@ final class ChatPanelInterfaceInteraction { let finishAudioRecording: (Bool) -> Void let setupMessageAutoremoveTimeout: () -> Void let sendSticker: (TelegramMediaFile) -> Void + let unblockPeer: () -> Void + let pinMessage: (MessageId) -> Void + let unpinMessage: () -> Void + let reportPeer: () -> Void + let dismissReportPeer: () -> Void + let deleteChat: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -45,6 +54,7 @@ final class ChatPanelInterfaceInteraction { self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId self.editMessage = editMessage self.beginMessageSearch = beginMessageSearch + self.navigateToMessage = navigateToMessage self.openPeerInfo = openPeerInfo self.togglePeerNotifications = togglePeerNotifications self.sendContextResult = sendContextResult @@ -55,6 +65,12 @@ final class ChatPanelInterfaceInteraction { self.finishAudioRecording = finishAudioRecording self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout self.sendSticker = sendSticker + self.unblockPeer = unblockPeer + self.pinMessage = pinMessage + self.unpinMessage = unpinMessage + self.reportPeer = reportPeer + self.dismissReportPeer = dismissReportPeer + self.deleteChat = deleteChat self.statuses = statuses } } diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift new file mode 100644 index 0000000000..972cd1ff92 --- /dev/null +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -0,0 +1,183 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) +private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() +}) + +final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { + private let account: Account + private let tapButton: HighlightTrackingButtonNode + private let closeButton: HighlightableButtonNode + private let lineNode: ASImageNode + private let titleNode: TextNode + private let textNode: TextNode + private let separatorNode: ASDisplayNode + + private let disposable = MetaDisposable() + private var currentMessageId: MessageId? + + private var currentLayout: CGFloat? + private var currentMessage: Message? + + private let queue = Queue() + + init(account: Account) { + self.account = account + + self.tapButton = HighlightTrackingButtonNode() + + self.closeButton = HighlightableButtonNode() + self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + self.separatorNode.isLayerBacked = true + + self.lineNode = ASImageNode() + self.lineNode.displayWithoutProcessing = true + self.lineNode.displaysAsynchronously = false + self.lineNode.image = lineImage + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = true + self.titleNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.displaysAsynchronously = true + self.textNode.isLayerBacked = true + + super.init() + + self.tapButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.textNode.layer.removeAnimation(forKey: "opacity") + strongSelf.textNode.alpha = 0.4 + strongSelf.lineNode.layer.removeAnimation(forKey: "opacity") + strongSelf.lineNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.textNode.alpha = 1.0 + strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.lineNode.alpha = 1.0 + strongSelf.lineNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) + self.addSubnode(self.tapButton) + + self.backgroundColor = UIColor(0xF5F6F8) + + self.addSubnode(self.separatorNode) + } + + deinit { + self.disposable.dispose() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + let panelHeight: CGFloat = 44.0 + + if self.currentMessageId != interfaceState.pinnedMessageId { + self.currentMessageId = interfaceState.pinnedMessageId + if let pinnedMessageId = interfaceState.pinnedMessageId { + self.disposable.set((singleMessageView(account: account, messageId: pinnedMessageId, loadIfNotExists: true) + |> deliverOnMainQueue).start(next: { [weak self] view in + if let strongSelf = self, let message = view.message { + strongSelf.currentMessage = message + if let currentLayout = strongSelf.currentLayout { + strongSelf.enqueueTransition(width: currentLayout, transition: .immediate, message: message) + } + } + })) + } + } + + let leftInset: CGFloat = 10.0 + let rightInset: CGFloat = 18.0 + + transition.updateFrame(node: self.lineNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 5.0), size: CGSize(width: 2.0, height: panelHeight - 10.0))) + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 16.0), size: closeButtonSize)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - rightInset - closeButtonSize.width - 4.0, height: panelHeight)) + + if self.currentLayout != width { + self.currentLayout = width + + if let currentMessage = self.currentMessage { + self.enqueueTransition(width: width, transition: .immediate, message: currentMessage) + } + } + + return panelHeight + } + + private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + queue.async { [weak self] in + let leftInset: CGFloat = 10.0 + let textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 18.0 + let textRightInset: CGFloat = 25.0 + + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: "Pinned message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), nil) + + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), nil) + + Queue.mainQueue().async { + if let strongSelf = self { + let _ = titleApply() + let _ = textApply() + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 5.0), size: titleLayout.size) + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 23.0), size: textLayout.size) + } + } + } + } + + @objc func tapped() { + if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { + interfaceInteraction.navigateToMessage(message.id) + } + } + + @objc func closePressed() { + self.interfaceInteraction?.unpinMessage() + } +} diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index b99ab33a26..0ee88dd744 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -114,23 +114,32 @@ enum ChatInputMode { } enum ChatTitlePanelContext: Comparable { + case pinnedMessage case chatInfo case requestInProgress case toastAlert(String) private var index: Int { switch self { - case .chatInfo: + case .pinnedMessage: return 0 - case .requestInProgress: + case .chatInfo: return 1 - case .toastAlert: + case .requestInProgress: return 2 + case .toastAlert: + return 3 } } static func ==(lhs: ChatTitlePanelContext, rhs: ChatTitlePanelContext) -> Bool { switch lhs { + case .pinnedMessage: + if case .pinnedMessage = rhs { + return true + } else { + return false + } case .chatInfo: if case .chatInfo = rhs { return true @@ -165,8 +174,12 @@ struct ChatPresentationInterfaceState: Equatable { let inputMode: ChatInputMode let titlePanelContexts: [ChatTitlePanelContext] let keyboardButtonsMessage: Message? + let pinnedMessageId: MessageId? + let peerIsBlocked: Bool + let canReportPeer: Bool let chatHistoryState: ChatHistoryNodeHistoryState? let botStartPayload: String? + let urlPreview: TelegramMediaWebpage? init() { self.interfaceState = ChatInterfaceState() @@ -176,11 +189,15 @@ struct ChatPresentationInterfaceState: Equatable { self.inputMode = .none self.titlePanelContexts = [] self.keyboardButtonsMessage = nil + self.pinnedMessageId = nil + self.peerIsBlocked = false + self.canReportPeer = false self.chatHistoryState = nil self.botStartPayload = nil + self.urlPreview = nil } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: TelegramMediaWebpage?) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState @@ -188,8 +205,12 @@ struct ChatPresentationInterfaceState: Equatable { self.inputMode = inputMode self.titlePanelContexts = titlePanelContexts self.keyboardButtonsMessage = keyboardButtonsMessage + self.pinnedMessageId = pinnedMessageId + self.peerIsBlocked = peerIsBlocked + self.canReportPeer = canReportPeer self.chatHistoryState = chatHistoryState self.botStartPayload = botStartPayload + self.urlPreview = urlPreview } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -231,6 +252,18 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.pinnedMessageId != rhs.pinnedMessageId { + return false + } + + if lhs.canReportPeer != rhs.canReportPeer { + return false + } + + if lhs.peerIsBlocked != rhs.peerIsBlocked { + return false + } + if lhs.chatHistoryState != rhs.chatHistoryState { return false } @@ -239,42 +272,66 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview { + if !lhsUrlPreview.isEqual(rhsUrlPreview) { + return false + } + } else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + } + + func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + } + + func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + } + + func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + } + + func updatedUrlPreview(_ urlPreview: TelegramMediaWebpage?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview) } } diff --git a/TelegramUI/ChatReportPeerTitlePanelNode.swift b/TelegramUI/ChatReportPeerTitlePanelNode.swift new file mode 100644 index 0000000000..30c3bddc24 --- /dev/null +++ b/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -0,0 +1,142 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore + +private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() +}) + + +private enum ChatReportPeerTitleButton { + case reportSpam + + var title: String { + switch self { + case .reportSpam: + return "Report spam" + } + } +} + +private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReportPeerTitleButton] { + return [.reportSpam] +} + +final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { + private let separatorNode: ASDisplayNode + + private let closeButton: HighlightableButtonNode + private var buttons: [(ChatReportPeerTitleButton, UIButton)] = [] + + override init() { + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + self.separatorNode.isLayerBacked = true + + self.closeButton = HighlightableButtonNode() + self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + super.init() + + self.backgroundColor = UIColor(0xF5F6F8) + + self.addSubnode(self.separatorNode) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + let panelHeight: CGFloat = 40.0 + + let rightInset: CGFloat = 18.0 + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - rightInset - closeButtonSize.width, y: 14.0), size: closeButtonSize)) + + let updatedButtons: [ChatReportPeerTitleButton] + if let _ = interfaceState.peer { + updatedButtons = peerButtons(interfaceState) + } else { + updatedButtons = [] + } + + var buttonsUpdated = false + if self.buttons.count != updatedButtons.count { + buttonsUpdated = true + } else { + for i in 0 ..< updatedButtons.count { + if self.buttons[i].0 != updatedButtons[i] { + buttonsUpdated = true + break + } + } + } + + if buttonsUpdated { + for (_, view) in self.buttons { + view.removeFromSuperview() + } + self.buttons.removeAll() + for button in updatedButtons { + let view = UIButton() + view.setTitle(button.title, for: []) + view.titleLabel?.font = Font.regular(16.0) + view.setTitleColor(UIColor(0x007ee5), for: []) + view.setTitleColor(UIColor(0x007ee5).withAlphaComponent(0.7), for: [.highlighted]) + view.addTarget(self, action: #selector(self.buttonPressed(_:)), for: [.touchUpInside]) + self.view.addSubview(view) + self.buttons.append((button, view)) + } + } + + if !self.buttons.isEmpty { + let buttonWidth = floor(width / CGFloat(self.buttons.count)) + var nextButtonOrigin: CGFloat = 0.0 + for (_, view) in self.buttons { + view.frame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) + nextButtonOrigin += buttonWidth + } + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + + return panelHeight + } + + @objc func buttonPressed(_ view: UIButton) { + for (button, buttonView) in self.buttons { + if buttonView === view { + switch button { + case .reportSpam: + self.interfaceInteraction?.reportPeer() + } + break + } + } + } + + @objc func closePressed() { + self.interfaceInteraction?.dismissReportPeer() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.closeButton.hitTest(CGPoint(x: point.x - self.closeButton.frame.minX, y: point.y - self.closeButton.frame.minY), with: event) { + return result + } + return super.hitTest(point, with: event) + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index ce9cd8d4e2..869b1618e7 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -219,6 +219,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var sendMessage: () -> Void = { } var updateHeight: () -> Void = { } + var updateActivity: () -> Void = { } + private var updatingInputState = false private var currentPlaceholder: String? @@ -803,8 +805,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { }) } + @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + self.updateActivity() + return true + } + @objc func sendButtonPressed() { - let text = self.textInputNode?.attributedText?.string ?? "" self.sendMessage() } diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift new file mode 100644 index 0000000000..c7fa39de78 --- /dev/null +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -0,0 +1,84 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class ChatUnblockInputPanelNode: ChatInputPanelNode { + private let button: HighlightableButtonNode + private let activityIndicator: UIActivityIndicatorView + + private var statusDisposable: Disposable? + + private var presentationInterfaceState = ChatPresentationInterfaceState() + + override var interfaceInteraction: ChatPanelInterfaceInteraction? { + didSet { + if self.statusDisposable == nil { + if let startingBot = self.interfaceInteraction?.statuses?.unblockingPeer { + self.statusDisposable = (startingBot |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if value != !strongSelf.activityIndicator.isHidden { + if value { + strongSelf.activityIndicator.isHidden = false + strongSelf.activityIndicator.startAnimating() + } else { + strongSelf.activityIndicator.isHidden = true + strongSelf.activityIndicator.stopAnimating() + } + } + } + }) + } + } + } + } + + override init() { + self.button = HighlightableButtonNode() + self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + self.activityIndicator.isHidden = true + + super.init() + + self.addSubnode(self.button) + self.view.addSubview(self.activityIndicator) + + self.button.setAttributedTitle(NSAttributedString(string: "Unblock", font: Font.regular(17.0), textColor: UIColor(0x007ee5)), for: []) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + } + + deinit { + self.statusDisposable?.dispose() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.button.view + } else { + return nil + } + } + + @objc func buttonPressed() { + self.interfaceInteraction?.unblockPeer() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + } + + let buttonSize = self.button.measure(CGSize(width: width - 80.0, height: 100.0)) + + let panelHeight: CGFloat = 47.0 + + self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + let indicatorSize = self.activityIndicator.bounds.size + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + + return 47.0 + } +} diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index 61b533c5e5..d116b17bd0 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -84,10 +84,35 @@ public class ComposeController: ViewController { self.contactsNode.openCreateNewSecretChat = { [weak self] in if let strongSelf = self { let controller = ContactSelectionController(account: strongSelf.account, title: "New Secret Chat") + strongSelf.createActionDisposable.set((controller.result + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] peerId in + if let strongSelf = self, let peerId = peerId { + controller?.dismissSearch() + controller?.displayNavigationActivity = true + strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in + if let strongSelf = self, let controller = controller { + controller.displayNavigationActivity = false + (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: strongSelf.account, peerId: peerId), animated: true) + } + }, error: { _ in + if let controller = controller { + controller.displayNavigationActivity = false + controller.present(standardTextAlertController(title: nil, text: "An error occurred.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window) + } + })) + } + })) (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } } + self.contactsNode.openCreateNewChannel = { [weak self] in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(createChannelController(account: strongSelf.account)) + } + } + self.displayNodeDidLoad() } diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index 2e41120826..f84fac127e 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -29,6 +29,18 @@ public class ContactSelectionController: ViewController { private let createActionDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable() + public var displayNavigationActivity: Bool = false { + didSet { + if self.displayNavigationActivity != oldValue { + if self.displayNavigationActivity { + self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()), animated: false) + } else { + self.navigationItem.setRightBarButton(nil, animated: false) + } + } + } + } + public init(account: Account, title: String, confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { self.account = account self.confirmation = confirmation @@ -56,7 +68,15 @@ public class ContactSelectionController: ViewController { @objc func cancelPressed() { self._result.set(.single(nil)) - self.contactsNode.animateOut() + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { + switch presentationArguments.presentationAnimation { + case .modalSheet: + self.contactsNode.animateOut() + case .none: + break + } + } } override public func loadDisplayNode() { @@ -151,9 +171,24 @@ public class ContactSelectionController: ViewController { if let strongSelf = self { if value { strongSelf._result.set(.single(peerId)) - strongSelf.contactsNode.animateOut() + strongSelf.dismiss() } } })) } + + public func dismiss() { + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { + switch presentationArguments.presentationAnimation { + case .modalSheet: + self.contactsNode.animateOut() + case .none: + break + } + } + } + + public func dismissSearch() { + self.deactivateSearch() + } } diff --git a/TelegramUI/ConvertToSupergroupController.swift b/TelegramUI/ConvertToSupergroupController.swift new file mode 100644 index 0000000000..405a544e40 --- /dev/null +++ b/TelegramUI/ConvertToSupergroupController.swift @@ -0,0 +1,151 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ConvertToSupergroupArguments { + let convert: () -> Void + + init(convert: @escaping () -> Void) { + self.convert = convert + } +} + +private enum ConvertToSupergroupSection: Int32 { + case info + case action +} + +private enum ConvertToSupergroupEntry: ItemListNodeEntry { + case info + case action + case actionInfo + + var section: ItemListSectionId { + switch self { + case .info: + return ConvertToSupergroupSection.info.rawValue + case .action, .actionInfo: + return ConvertToSupergroupSection.action.rawValue + } + } + + var stableId: Int32 { + switch self { + case .info: + return 0 + case .action: + return 1 + case .actionInfo: + return 2 + } + } + + static func ==(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool { + return lhs.stableId == rhs.stableId + } + + static func <(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: ConvertToSupergroupArguments) -> ListViewItem { + switch self { + case .info: + return ItemListTextItem(text: "In supergroups:\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group", sectionId: self.section) + case .action: + return ItemListActionItem(title: "Convert to Supergroup", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.convert() + }) + case .actionInfo: + return ItemListTextItem(text: "Note: this action can't be undone", sectionId: self.section) + } + } +} + +private struct ConvertToSupergroupState: Equatable { + let isConverting: Bool + + init() { + self.isConverting = false + } + + init(isConverting: Bool) { + self.isConverting = isConverting + } + + static func ==(lhs: ConvertToSupergroupState, rhs: ConvertToSupergroupState) -> Bool { + if lhs.isConverting != rhs.isConverting { + return false + } + return true + } +} + +private func convertToSupergroupEntries() -> [ConvertToSupergroupEntry] { + var entries: [ConvertToSupergroupEntry] = [] + + entries.append(.info) + entries.append(.action) + entries.append(.actionInfo) + + return entries +} + +public func convertToSupergroupController(account: Account, peerId: PeerId) -> ViewController { + var replaceControllerImpl: ((ViewController) -> Void)? + + let statePromise = ValuePromise(ConvertToSupergroupState(), ignoreRepeated: true) + let stateValue = Atomic(value: ConvertToSupergroupState()) + let updateState: ((ConvertToSupergroupState) -> ConvertToSupergroupState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let convertDisposable = MetaDisposable() + actionsDisposable.add(convertDisposable) + + let arguments = ConvertToSupergroupArguments(convert: { + var alreadyConverting = false + updateState { state in + if state.isConverting { + alreadyConverting = true + } + return ConvertToSupergroupState(isConverting: true) + } + + if !alreadyConverting { + convertDisposable.set((convertGroupToSupergroup(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in + replaceControllerImpl?(ChatController(account: account, peerId: createdPeerId)) + })) + } + }) + + let signal = statePromise.get() + |> deliverOnMainQueue + |> map { state -> (ItemListControllerState, (ItemListNodeState, ConvertToSupergroupEntry.ItemGenerationArguments)) in + + var rightNavigationButton: ItemListNavigationButton? + if state.isConverting { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } + + let controllerState = ItemListControllerState(title: "Supergroup", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) + let listState = ItemListNodeState(entries: convertToSupergroupEntries(), style: .blocks) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + replaceControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: true) + } + } + return controller +} diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift new file mode 100644 index 0000000000..bf3f8db2b9 --- /dev/null +++ b/TelegramUI/CreateChannelController.swift @@ -0,0 +1,234 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private struct CreateChannelArguments { + let account: Account + + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updateEditingDescriptionText: (String) -> Void + let done: () -> Void +} + +private enum CreateChannelSection: Int32 { + case info + case description +} + +private enum CreateChannelEntry: ItemListNodeEntry { + case channelInfo(Peer?, ItemListAvatarAndNameInfoItemState) + case setProfilePhoto + + case descriptionSetup(text: String) + case descriptionInfo + + var section: ItemListSectionId { + switch self { + case .channelInfo, .setProfilePhoto: + return CreateChannelSection.info.rawValue + case .descriptionSetup, .descriptionInfo: + return CreateChannelSection.description.rawValue + } + } + + var stableId: Int32 { + switch self { + case .channelInfo: + return 0 + case .setProfilePhoto: + return 1 + case .descriptionSetup: + return 2 + case .descriptionInfo: + return 3 + } + } + + static func ==(lhs: CreateChannelEntry, rhs: CreateChannelEntry) -> Bool { + switch lhs { + case let .channelInfo(lhsPeer, lhsEditingState): + if case let .channelInfo(rhsPeer, rhsEditingState) = rhs { + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer != nil) != (rhsPeer != 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 let .descriptionSetup(text): + if case .descriptionSetup(text) = rhs { + return true + } else { + return false + } + case .descriptionInfo: + if case .descriptionInfo = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: CreateChannelEntry, rhs: CreateChannelEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: CreateChannelArguments) -> ListViewItem { + switch self { + case let .channelInfo(peer, state): + return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }) + case .setProfilePhoto: + return ItemListActionItem(title: "Set Profile Photo", kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + + }) + case let .descriptionSetup(text): + return ItemListMultilineInputItem(text: text, placeholder: "Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in + arguments.updateEditingDescriptionText(updatedText) + }, action: { + + }) + case .descriptionInfo: + return ItemListTextItem(text: "You can provide an optional description for your channel.", sectionId: self.section) + } + } +} + +private struct CreateChannelState: Equatable { + let creating: Bool + let editingName: ItemListAvatarAndNameInfoItemName + let editingDescriptionText: String + + init(creating: Bool, editingName: ItemListAvatarAndNameInfoItemName, editingDescriptionText: String) { + self.creating = creating + self.editingName = editingName + self.editingDescriptionText = editingDescriptionText + } + + init() { + self.creating = false + self.editingName = .title(title: "") + self.editingDescriptionText = "" + } + + static func ==(lhs: CreateChannelState, rhs: CreateChannelState) -> Bool { + if lhs.creating != rhs.creating { + return false + } + if lhs.editingName != rhs.editingName { + return false + } + if lhs.editingDescriptionText != rhs.editingDescriptionText { + return false + } + return true + } +} + +private func CreateChannelEntries(state: CreateChannelState) -> [CreateChannelEntry] { + var entries: [CreateChannelEntry] = [] + + let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) + + let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) + + entries.append(.channelInfo(peer, groupInfoState)) + entries.append(.setProfilePhoto) + + entries.append(.descriptionSetup(text: state.editingDescriptionText)) + entries.append(.descriptionInfo) + + return entries +} + +public func createChannelController(account: Account) -> ViewController { + let initialState = CreateChannelState() + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((CreateChannelState) -> CreateChannelState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var replaceControllerImpl: ((ViewController) -> Void)? + + let actionsDisposable = DisposableSet() + + let arguments = CreateChannelArguments(account: account, updateEditingName: { editingName in + updateState { current in + return CreateChannelState(creating: current.creating, editingName: editingName, editingDescriptionText: current.editingDescriptionText) + } + }, updateEditingDescriptionText: { text in + updateState { current in + return CreateChannelState(creating: current.creating, editingName: current.editingName, editingDescriptionText: text) + } + }, done: { + let (creating, title, description) = stateValue.with { state -> (Bool, String, String) in + return (state.creating, state.editingName.composedTitle, state.editingDescriptionText) + } + + if !creating && !title.isEmpty { + updateState { current in + return CreateChannelState(creating: true, editingName: current.editingName, editingDescriptionText: current.editingDescriptionText) + } + + actionsDisposable.add((createChannel(account: account, title: title, description: description.isEmpty ? nil : description) |> deliverOnMainQueue |> afterDisposed { + Queue.mainQueue().async { + updateState { current in + return CreateChannelState(creating: false, editingName: current.editingName, editingDescriptionText: current.editingDescriptionText) + } + } + }).start(next: { peerId in + if let peerId = peerId { + let controller = channelVisibilityController(account: account, peerId: peerId, mode: .initialSetup) + replaceControllerImpl?(controller) + } + }, error: { _ in + + })) + } + }) + + let signal = statePromise.get() + |> map { state -> (ItemListControllerState, (ItemListNodeState, CreateChannelEntry.ItemGenerationArguments)) in + + let rightNavigationButton: ItemListNavigationButton + if state.creating { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + arguments.done() + }) + } + + let controllerState = ItemListControllerState(title: "Create Channel", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) + let listState = ItemListNodeState(entries: CreateChannelEntries(state: state), style: .blocks) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + replaceControllerImpl = { [weak controller] value in + (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) + } + return controller +} diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index b917d46b80..58fc242959 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -104,15 +104,19 @@ private enum CreateGroupEntry: ItemListNodeEntry { }) case let .member(_, peer, presence): - 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 }) + return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: nil, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in }) } } } private struct CreateGroupState: Equatable { + let creating: Bool let editingName: ItemListAvatarAndNameInfoItemName static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool { + if lhs.creating != rhs.creating { + return false + } if lhs.editingName != rhs.editingName { return false } @@ -166,7 +170,7 @@ private func createGroupEntries(state: CreateGroupState, peerIds: [PeerId], view } public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewController { - let initialState = CreateGroupState(editingName: .title(title: "")) + let initialState = CreateGroupState(creating: false, editingName: .title(title: "")) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in @@ -178,16 +182,25 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo let actionsDisposable = DisposableSet() let arguments = CreateGroupArguments(account: account, updateEditingName: { editingName in - updateState { _ in - return CreateGroupState(editingName: editingName) + updateState { current in + return CreateGroupState(creating: current.creating, editingName: editingName) } }, done: { - let title = stateValue.with { state -> String in - return state.editingName.composedTitle + let (creating, title) = stateValue.with { state -> (Bool, String) in + return (state.creating, state.editingName.composedTitle) } - if !title.isEmpty { - actionsDisposable.add((createGroup(account: account, title: title, peerIds: peerIds) |> deliverOnMainQueue).start(next: { peerId in + if !creating && !title.isEmpty { + updateState { current in + return CreateGroupState(creating: true, editingName: current.editingName) + } + actionsDisposable.add((createGroup(account: account, title: title, peerIds: peerIds) |> deliverOnMainQueue |> afterDisposed { + Queue.mainQueue().async { + updateState { current in + return CreateGroupState(creating: false, editingName: current.editingName) + } + } + }).start(next: { peerId in if let peerId = peerId { let controller = ChatController(account: account, peerId: peerId) replaceControllerImpl?(controller) @@ -199,9 +212,14 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo let signal = combineLatest(statePromise.get(), account.postbox.multiplePeersView(peerIds)) |> map { state, view -> (ItemListControllerState, (ItemListNodeState, CreateGroupEntry.ItemGenerationArguments)) in - let rightNavigationButton = ItemListNavigationButton(title: "Create", style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { - arguments.done() - }) + let rightNavigationButton: ItemListNavigationButton + if state.creating { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: "Create", style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + arguments.done() + }) + } let controllerState = ItemListControllerState(title: "Create Group", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) let listState = ItemListNodeState(entries: createGroupEntries(state: state, peerIds: peerIds, view: view), style: .blocks) @@ -212,6 +230,7 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo } let controller = ItemListController(signal) + controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift new file mode 100644 index 0000000000..61197c0f78 --- /dev/null +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -0,0 +1,51 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class DeleteChatInputPanelNode: ChatInputPanelNode { + private let button: HighlightableButtonNode + + private var presentationInterfaceState = ChatPresentationInterfaceState() + + override init() { + self.button = HighlightableButtonNode() + self.button.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.button.view + } else { + return nil + } + } + + @objc func buttonPressed() { + self.interfaceInteraction?.deleteChat() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + + self.button.setAttributedTitle(NSAttributedString(string: "Delete and Exit", font: Font.regular(17.0), textColor: UIColor(0xff3b30)), for: []) + } + + let buttonSize = self.button.measure(CGSize(width: width - 10.0, height: 100.0)) + + let panelHeight: CGFloat = 47.0 + + self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + return panelHeight + } +} diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 3027b3dc00..42416676df 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -67,7 +67,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa }) semaphore.wait() } else { - let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, complete: true) + let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false)) let range = context.readingOffset ..< (context.readingOffset + readCount) let semaphore = DispatchSemaphore(value: 0) let _ = data.start(next: { next in diff --git a/TelegramUI/GenerateTextEntities.swift b/TelegramUI/GenerateTextEntities.swift index 0d1ec44888..46ea4278db 100644 --- a/TelegramUI/GenerateTextEntities.swift +++ b/TelegramUI/GenerateTextEntities.swift @@ -67,13 +67,18 @@ func generateTextEntities(_ text: String) -> [MessageTextEntity] { entities.append(MessageTextEntity(range: indexRange, type: entityType)) } } + var previousScalar: UnicodeScalar? while index != unicodeScalars.endIndex { let c = unicodeScalars[index] if c == "/" { - if let (type, range) = currentEntity { - commitEntity(unicodeScalars, type, range, &entities) + if previousScalar != nil && !identifierDelimiterSet.contains(previousScalar!) { + currentEntity = nil + } else { + if let (type, range) = currentEntity { + commitEntity(unicodeScalars, type, range, &entities) + } + currentEntity = (.command, index ..< index) } - currentEntity = (.command, index ..< index) } else if c == "@" { if let (type, range) = currentEntity { if case .command = type { @@ -115,6 +120,7 @@ func generateTextEntities(_ text: String) -> [MessageTextEntity] { } } index = unicodeScalars.index(after: index) + previousScalar = c } if let (type, range) = currentEntity { commitEntity(unicodeScalars, type, range, &entities) diff --git a/TelegramUI/GroupAdminsController.swift b/TelegramUI/GroupAdminsController.swift new file mode 100644 index 0000000000..4c30e3d20b --- /dev/null +++ b/TelegramUI/GroupAdminsController.swift @@ -0,0 +1,289 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class GroupAdminsControllerArguments { + let account: Account + + let updateAllAreAdmins: (Bool) -> Void + let updatePeerIsAdmin: (PeerId, Bool) -> Void + + init(account: Account, updateAllAreAdmins: @escaping (Bool) -> Void, updatePeerIsAdmin: @escaping (PeerId, Bool) -> Void) { + self.account = account + self.updateAllAreAdmins = updateAllAreAdmins + self.updatePeerIsAdmin = updatePeerIsAdmin + } +} + +private enum GroupAdminsSection: Int32 { + case allAdmins + case peers +} + +private enum GroupAdminsEntryStableId: 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: GroupAdminsEntryStableId, rhs: GroupAdminsEntryStableId) -> 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 GroupAdminsEntry: ItemListNodeEntry { + case allAdmins(Bool) + case allAdminsInfo(String) + case peerItem(Int32, Peer, PeerPresence?, Bool, Bool) + + var section: ItemListSectionId { + switch self { + case .allAdmins, .allAdminsInfo: + return GroupAdminsSection.allAdmins.rawValue + case .peerItem: + return GroupAdminsSection.peers.rawValue + } + } + + var stableId: GroupAdminsEntryStableId { + switch self { + case .allAdmins: + return .index(0) + case .allAdminsInfo: + return .index(1) + case let .peerItem(_, peer, _, _, _): + return .peer(peer.id) + } + } + + static func ==(lhs: GroupAdminsEntry, rhs: GroupAdminsEntry) -> Bool { + switch lhs { + case let .allAdmins(value): + if case .allAdmins(value) = rhs { + return true + } else { + return false + } + case let .allAdminsInfo(text): + if case .allAdminsInfo(text) = rhs { + return true + } else { + return false + } + case let .peerItem(lhsIndex, lhsPeer, lhsPresence, lhsToggled, lhsEnabled): + if case let .peerItem(rhsIndex, rhsPeer, rhsPresence, rhsToggled, rhsEnabled) = rhs { + if lhsIndex != rhsIndex { + 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 lhsToggled != rhsToggled { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: GroupAdminsEntry, rhs: GroupAdminsEntry) -> Bool { + switch lhs { + case .allAdmins: + return true + case .allAdminsInfo: + switch rhs { + case .allAdmins: + return false + default: + return true + } + case let .peerItem(index, _, _, _, _): + switch rhs { + case let .peerItem(rhsIndex, _, _, _, _): + return index < rhsIndex + case .allAdmins, .allAdminsInfo: + return false + } + } + } + + func item(_ arguments: GroupAdminsControllerArguments) -> ListViewItem { + switch self { + case let .allAdmins(value): + return ItemListSwitchItem(title: "All Members Are Admins", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + + }) + case let .allAdminsInfo(text): + return ItemListTextItem(text: text, sectionId: self.section) + case let .peerItem(_, peer, presence, toggled, enabled): + return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: nil, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in }) + } + } +} + +private struct GroupAdminsControllerState: Equatable { + let updatingAllAdminsValue: Bool? + let updatingAdminValue: [PeerId: Bool] + + init() { + self.updatingAllAdminsValue = nil + self.updatingAdminValue = [:] + } + + init(updatingAllAdminsValue: Bool?, updatingAdminValue: [PeerId: Bool]) { + self.updatingAllAdminsValue = updatingAllAdminsValue + self.updatingAdminValue = updatingAdminValue + } + + static func ==(lhs: GroupAdminsControllerState, rhs: GroupAdminsControllerState) -> Bool { + if lhs.updatingAllAdminsValue != rhs.updatingAllAdminsValue { + return false + } + if lhs.updatingAdminValue != rhs.updatingAdminValue { + return false + } + + return true + } + + func withUpdatedUpdatingAllAdminsValue(_ updatingAllAdminsValue: Bool?) -> GroupAdminsControllerState { + return GroupAdminsControllerState(updatingAllAdminsValue: updatingAllAdminsValue, updatingAdminValue: self.updatingAdminValue) + } + + func withUpdatedUpdatingAdminValue(_ updatingAdminValue: [PeerId: Bool]) -> GroupAdminsControllerState { + return GroupAdminsControllerState(updatingAllAdminsValue: self.updatingAllAdminsValue, updatingAdminValue: updatingAdminValue) + } +} + +private func groupAdminsControllerEntries(account: Account, view: PeerView, state: GroupAdminsControllerState) -> [GroupAdminsEntry] { + var entries: [GroupAdminsEntry] = [] + + if let peer = view.peers[view.peerId] as? TelegramGroup, let cachedData = view.cachedData as? CachedGroupData, let participants = cachedData.participants { + entries.append(.allAdmins(!peer.flags.contains(.adminsEnabled))) + if peer.flags.contains(.adminsEnabled) { + entries.append(.allAdminsInfo("Only admins can add and remove members, edit name and photo of this group.")) + } else { + entries.append(.allAdminsInfo("Group members can add new members, edit name and photo of this group.")) + } + + let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in + let lhsInvitedAt: Int32 + switch lhs { + case let .admin(_, _, invitedAt): + lhsInvitedAt = invitedAt + case .creator(_): + lhsInvitedAt = Int32.max + case let .member(_, _, invitedAt): + lhsInvitedAt = invitedAt + } + + let rhsInvitedAt: Int32 + switch rhs { + case let .admin(_, _, invitedAt): + rhsInvitedAt = invitedAt + case .creator(_): + rhsInvitedAt = Int32.max + case let .member(_, _, invitedAt): + rhsInvitedAt = invitedAt + } + return lhsInvitedAt > rhsInvitedAt + }) + + var index: Int32 = 0 + for participant in sortedParticipants { + if let peer = view.peers[participant.peerId] { + entries.append(.peerItem(index, peer, view.peerPresences[participant.peerId], false, false)) + index += 1 + } + } + } + + return entries +} + +public func groupAdminsController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(GroupAdminsControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: GroupAdminsControllerState()) + let updateState: ((GroupAdminsControllerState) -> GroupAdminsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + let actionsDisposable = DisposableSet() + + let toggleAllAdminsDisposable = MetaDisposable() + actionsDisposable.add(toggleAllAdminsDisposable) + + let toggleAdminsMetaDisposable = MetaDisposable() + let toggleAdminsDisposable = DisposableSet() + toggleAdminsMetaDisposable.set(toggleAdminsDisposable) + actionsDisposable.add(toggleAdminsMetaDisposable) + + let arguments = GroupAdminsControllerArguments(account: account, updateAllAreAdmins: { value in + + }, updatePeerIsAdmin: { peerId, value in + + }) + + let peerView = account.viewTracker.peerView(peerId) + + let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { state, view -> (ItemListControllerState, (ItemListNodeState, GroupAdminsEntry.ItemGenerationArguments)) in + + var emptyStateItem: ItemListControllerEmptyStateItem? + if view.cachedData == nil { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + } + + let controllerState = ItemListControllerState(title: "Admins", leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: true) + let listState = ItemListNodeState(entries: groupAdminsControllerEntries(account: account, view: view, state: state), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window, with: p) + } + } + return controller +} diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 46eded33e0..105e4eb50d 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -12,22 +12,30 @@ private final class GroupInfoArguments { let pushController: (ViewController) -> Void let presentController: (ViewController, ViewControllerPresentationArguments) -> Void + let changeNotificationMuteSettings: () -> Void + let openSharedMedia: () -> Void + let openAdminManagement: () -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let updateEditingDescriptionText: (String) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let addMember: () -> Void let removePeer: (PeerId) -> Void + let convertToSupergroup: () -> 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) { + init(account: Account, peerId: PeerId, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void) { self.account = account self.peerId = peerId self.pushController = pushController self.presentController = presentController + self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.openSharedMedia = openSharedMedia + self.openAdminManagement = openAdminManagement self.updateEditingName = updateEditingName self.updateEditingDescriptionText = updateEditingDescriptionText self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.addMember = addMember self.removePeer = removePeer + self.convertToSupergroup = convertToSupergroup } } @@ -84,6 +92,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case link(String) case sharedMedia case notifications(settings: PeerNotificationSettings?) + case adminManagement case groupTypeSetup(isPublic: Bool) case groupDescriptionSetup(text: String) case groupManagementInfoLabel(text: String) @@ -91,6 +100,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { 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 convertToSupergroup case leave var section: ItemListSectionId { @@ -99,7 +109,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return GroupInfoSection.info.rawValue case .about, .link: return GroupInfoSection.about.rawValue - case .sharedMedia, .notifications: + case .sharedMedia, .notifications, .adminManagement: return GroupInfoSection.sharedMediaAndNotifications.rawValue case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel: return GroupInfoSection.infoManagement.rawValue @@ -107,7 +117,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return GroupInfoSection.memberManagement.rawValue case .addMember, .member: return GroupInfoSection.members.rawValue - case .leave: + case .convertToSupergroup, .leave: return GroupInfoSection.leave.rawValue } } @@ -137,7 +147,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case .setGroupPhoto, .sharedMedia, .leave: + case .setGroupPhoto, .sharedMedia, .leave, .convertToSupergroup, .adminManagement: return lhs.sortIndex == rhs.sortIndex case let .about(text): if case .about(text) = rhs { @@ -255,22 +265,26 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 4 case .sharedMedia: return 5 - case .groupTypeSetup: + case .adminManagement: return 6 - case .groupDescriptionSetup: + case .groupTypeSetup: return 7 - case .groupManagementInfoLabel: + case .groupDescriptionSetup: return 8 - case .membersAdmins: + case .groupManagementInfoLabel: return 9 - case .membersBlacklist: + case .membersAdmins: return 10 - case .addMember: + case .membersBlacklist: return 11 + case .addMember: + return 12 case let .member(index, _, _, _, _, _, _): return 20 + index + case .convertToSupergroup: + return 100000 case .leave: - return 1000000 + return 100000 + 1 } } @@ -300,11 +314,15 @@ private enum GroupInfoEntry: ItemListNodeEntry { label = "Enabled" } return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { - //interaction.changeNotificationMuteSettings() + arguments.changeNotificationMuteSettings() }) case .sharedMedia: return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { - //interaction.openSharedMedia() + arguments.openSharedMedia() + }) + case .adminManagement: + return ItemListDisclosureItem(title: "Add Admins", label: "", sectionId: self.section, style: .blocks, action: { + arguments.openAdminManagement() }) case let .addMember(editing): return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section, editing: editing, action: { @@ -312,10 +330,10 @@ private enum GroupInfoEntry: ItemListNodeEntry { }) 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)) + arguments.presentController(channelVisibilityController(account: arguments.account, peerId: arguments.peerId, mode: .generic), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) }) case let .groupDescriptionSetup(text): - return ItemListMultilineInputItem(text: text, placeholder: "Group Description", sectionId: self.section, textUpdated: { updatedText in + return ItemListMultilineInputItem(text: text, placeholder: "Group Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { @@ -336,7 +354,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { 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: { + return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: label, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { if let infoController = peerInfoController(account: arguments.account, peer: peer) { arguments.pushController(infoController) } @@ -345,6 +363,10 @@ private enum GroupInfoEntry: ItemListNodeEntry { }, removePeer: { peerId in arguments.removePeer(peerId) }) + case .convertToSupergroup: + return ItemListActionItem(title: "Convert to Supergroup", kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + arguments.convertToSupergroup() + }) case .leave: return ItemListActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { }) @@ -519,7 +541,9 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo } if let editingState = state.editingState { - if let cachedChannelData = view.cachedData as? CachedChannelData { + if let group = view.peers[view.peerId] as? TelegramGroup, case .creator = group.role { + entries.append(.adminManagement) + } else if let cachedChannelData = view.cachedData as? CachedChannelData { entries.append(GroupInfoEntry.groupTypeSetup(isPublic: isPublic)) entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText)) @@ -778,11 +802,14 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo if let group = view.peers[view.peerId] as? TelegramGroup { if case .Member = group.membership { - entries.append(GroupInfoEntry.leave) + if case .creator = group.role, state.editingState != nil { + entries.append(.convertToSupergroup) + } + entries.append(.leave) } } else if let channel = view.peers[view.peerId] as? TelegramChannel { if case .member = channel.participationStatus { - entries.append(GroupInfoEntry.leave) + entries.append(.leave) } } @@ -849,10 +876,61 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl let removeMemberDisposable = MetaDisposable() actionsDisposable.add(removeMemberDisposable) + let changeMuteSettingsDisposable = MetaDisposable() + actionsDisposable.add(changeMuteSettingsDisposable) + let arguments = GroupInfoArguments(account: account, peerId: peerId, pushController: { controller in pushControllerImpl?(controller) }, presentController: { controller, presentationArguments in presentControllerImpl?(controller, presentationArguments) + }, changeNotificationMuteSettings: { + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let notificationAction: (Int32) -> Void = { muteUntil in + let muteState: PeerMuteState + if muteUntil <= 0 { + muteState = .unmuted + } else if muteUntil == Int32.max { + muteState = .muted(until: Int32.max) + } else { + muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + } + changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Enable", action: { + dismissAction() + notificationAction(0) + }), + ActionSheetButtonItem(title: "Mute for 1 hour", action: { + dismissAction() + notificationAction(1 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 8 hours", action: { + dismissAction() + notificationAction(8 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 2 days", action: { + dismissAction() + notificationAction(2 * 24 * 60 * 60) + }), + ActionSheetButtonItem(title: "Disable", action: { + dismissAction() + notificationAction(Int32.max) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, openSharedMedia: { + if let controller = peerSharedMediaController(account: account, peerId: peerId) { + pushControllerImpl?(controller) + } + }, openAdminManagement: { + pushControllerImpl?(groupAdminsController(account: account, peerId: peerId)) }, updateEditingName: { editingName in updateState { state in if let editingState = state.editingState { @@ -1022,6 +1100,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } } removeMemberDisposable.set(signal.start()) + }, convertToSupergroup: { + pushControllerImpl?(convertToSupergroupController(account: account, peerId: peerId)) }) let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift index db3c9b53f3..721e4d1892 100644 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -114,6 +114,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { super.init(account: account) + self.placement = .overTextInput self.isOpaque = false self.addSubnode(self.backgroundNode) diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index ca21f6a460..e5c5999079 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -7,6 +7,7 @@ enum ItemListActionKind { case generic case destructive case neutral + case disabled } enum ItemListActionAlignment { @@ -127,6 +128,8 @@ class ItemListActionItemNode: ListViewItemNode { textColor = UIColor(0x007ee5) case .neutral: textColor = .black + case .disabled: + textColor = UIColor(0x8e8e93) } let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 23789d7edc..d941bb0757 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -36,7 +36,7 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { var isEmpty: Bool { switch self { case let .personName(firstName, lastName): - return !firstName.isEmpty || !lastName.isEmpty + return firstName.isEmpty case let .title(title): return title.isEmpty } @@ -363,6 +363,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let inputFirstField = TextFieldNodeView() inputFirstField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] //inputFirstField.backgroundColor = UIColor.lightGray + inputFirstField.autocorrectionType = .no inputFirstField.attributedPlaceholder = NSAttributedString(string: "First Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(17.0), textColor: UIColor.black) strongSelf.inputFirstField = inputFirstField @@ -375,6 +376,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if strongSelf.inputSecondField == nil { let inputSecondField = TextFieldNodeView() inputSecondField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + inputSecondField.autocorrectionType = .no inputSecondField.attributedPlaceholder = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: UIColor.black) strongSelf.inputSecondField = inputSecondField @@ -406,6 +408,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let inputFirstField = TextFieldNodeView() inputFirstField.typingAttributes = [NSFontAttributeName: Font.regular(19.0)] //inputFirstField.backgroundColor = UIColor.lightGray + inputFirstField.autocorrectionType = .no inputFirstField.attributedPlaceholder = NSAttributedString(string: "Title", font: Font.regular(19.0), textColor: UIColor(0xc8c8ce)) inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: UIColor.black) strongSelf.inputFirstField = inputFirstField diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 36453105df..a22fe3596b 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -45,6 +45,8 @@ final class ItemListController: ViewController { private var rightNavigationButtonTitleAndStyle: (String, ItemListNavigationButtonStyle)? private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?) = (nil, nil) + private var didPlayPresentationAnimation = false + init(_ state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { self.state = state @@ -128,7 +130,8 @@ final class ItemListController: ViewController { (self.displayNode as! ItemListNode).listNode.preloadPages = true - if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true if case .modalSheet = presentationArguments.presentationAnimation { (self.displayNode as! ItemListNode).animateIn() } diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index f5759154d3..0e53375112 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -111,7 +111,6 @@ class ItemListDisclosureItemNode: ListViewItemNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) return { item, width, neighbors in - let sectionInset: CGFloat = 22.0 let rightInset: CGFloat = 34.0 let contentSize: CGSize diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index 0483518dbd..820bd33ea7 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -7,13 +7,15 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { let text: String let placeholder: String let sectionId: ItemListSectionId + let style: ItemListStyle let action: () -> Void let textUpdated: (String) -> Void - init(text: String, placeholder: String, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(text: String, placeholder: String, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { self.text = text self.placeholder = placeholder self.sectionId = sectionId + self.style = style self.textUpdated = textUpdated self.action = action } @@ -101,7 +103,13 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) return { item, width, neighbors in - let leftInset: CGFloat = 16.0 + let leftInset: CGFloat + switch item.style { + case .blocks: + leftInset = 16.0 + case .plain: + leftInset = 35.0 + } var measureText = item.text if measureText.hasSuffix("\n") || measureText.isEmpty { @@ -190,14 +198,24 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega let insets = self.insets let width = self.bounds.size.width let contentSize = CGSize(width: width, height: currentValue - insets.top - insets.bottom) - let leftInset: CGFloat = 16.0 - let textTopInset: CGFloat = 11.0 - let textBottomInset: CGFloat = 11.0 - self.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))) - self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight)) - - self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, width - leftInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) + if let item = self.item { + let leftInset: CGFloat + switch item.style { + case .blocks: + leftInset = 16.0 + case .plain: + leftInset = 35.0 + } + + let textTopInset: CGFloat = 11.0 + let textBottomInset: CGFloat = 11.0 + + self.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))) + self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight)) + + self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, width - leftInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) + } } func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index c75d5be7c2..06f059b78b 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -25,7 +25,7 @@ struct ItemListPeerItemEditing: Equatable { } enum ItemListPeerItemText { - case activity + case presence case text(String) case none } @@ -37,19 +37,21 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let text: ItemListPeerItemText let label: String? let editing: ItemListPeerItemEditing + let switchValue: Bool? 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?, text: ItemListPeerItemText, label: String?, editing: ItemListPeerItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) { + init(account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: String?, editing: ItemListPeerItemEditing, switchValue: Bool?, 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.switchValue = switchValue self.enabled = enabled self.sectionId = sectionId self.action = action @@ -118,6 +120,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private let titleNode: TextNode private let labelNode: TextNode private let statusNode: TextNode + private var switchNode: SwitchNode? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ItemListPeerItem, CGFloat, ItemListNeighbors)? @@ -193,6 +196,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { var currentDisabledOverlayNode = self.disabledOverlayNode + var currentSwitchNode = self.switchNode + return { item, width, neighbors in var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? @@ -205,6 +210,14 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { peerRevealOptions = [] } + if let switchValue = item.switchValue { + if currentSwitchNode == nil { + currentSwitchNode = SwitchNode() + } + } else { + currentSwitchNode = nil + } + if let user = item.peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() @@ -226,7 +239,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } switch item.text { - case .activity: + case .presence: if let presence = item.presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) diff --git a/TelegramUI/PeerInfoController.swift b/TelegramUI/PeerInfoController.swift index 3ad081224f..28e8033a69 100644 --- a/TelegramUI/PeerInfoController.swift +++ b/TelegramUI/PeerInfoController.swift @@ -10,15 +10,17 @@ func peerInfoController(account: Account, peer: Peer) -> ViewController? { } else if let channel = peer as? TelegramChannel { if case .group = channel.info { return groupInfoController(account: account, peerId: peer.id) + } else { + return channelInfoController(account: account, peerId: peer.id) } + } else if let _ = peer as? TelegramUser { + return userInfoController(account: account, peerId: peer.id) } return nil } - - final class PeerInfoControllerInteraction { let updateState: ((PeerInfoState?) -> PeerInfoState?) -> Void let openSharedMedia: () -> Void diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 826ec0dfbc..e11e1c5c81 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -202,6 +202,7 @@ public class PeerMediaCollectionController: ViewController { }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { _, _ in }, beginMessageSearch: { + }, navigateToMessage: { _ in }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _ in @@ -212,6 +213,12 @@ public class PeerMediaCollectionController: ViewController { }, finishAudioRecording: { _ in }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in + }, unblockPeer: { + }, pinMessage: { _ in + }, unpinMessage: { + }, reportPeer: { + }, dismissReportPeer: { + }, deleteChat: { }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index e352e1432a..d0d0de4eb3 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -99,7 +99,7 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pro } - let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, complete: !progressive) |> map { next -> ((Data, String)?, Bool) in + let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false)) |> map { next -> ((Data, String)?, Bool) in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) return (data == nil ? nil : (data!, next.path), next.complete) } diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index c33fdb4bd6..52d62286df 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,7 +4,7 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -158,7 +158,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } } - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift new file mode 100644 index 0000000000..1c6415f1d7 --- /dev/null +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -0,0 +1,64 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { + private let button: HighlightableButtonNode + + private var statusDisposable: Disposable? + + private var presentationInterfaceState = ChatPresentationInterfaceState() + + override init() { + self.button = HighlightableButtonNode() + self.button.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + } + + deinit { + self.statusDisposable?.dispose() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.button.view + } else { + return nil + } + } + + @objc func buttonPressed() { + self.interfaceInteraction?.unblockPeer() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + self.presentationInterfaceState = interfaceState + + if let peer = interfaceState.peer as? TelegramSecretChat { + switch peer.embeddedState { + case .handshake: + self.button.setAttributedTitle(NSAttributedString(string: "Exchanging encryption keys...", font: Font.regular(15.0), textColor: .black), for: []) + case .active, .terminated: + break + } + } + } + + let buttonSize = self.button.measure(CGSize(width: width - 10.0, height: 100.0)) + + let panelHeight: CGFloat = 47.0 + + self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + return panelHeight + } +} diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index ee7204c1be..a3529ce64a 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -219,7 +219,7 @@ private enum SettingsEntry: ItemListNodeEntry { }) case let .username(address): return ItemListDisclosureItem(title: "Username", label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - + arguments.presentController(usernameSetupController(account: arguments.account)) }) case .askAQuestion: return ItemListDisclosureItem(title: "Ask a Question", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { @@ -399,7 +399,7 @@ public func settingsController(account: Account, accountManager: AccountManager) (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value in - controller?.present(value, in: .window) + controller?.present(value, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } return controller } diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index 07a2acaa28..479d8edb41 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -28,27 +28,27 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba } string.addAttribute(TextNode.UrlAttribute, value: nsString!.substring(with: range), range: range) case .Email: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.UrlAttribute, value: "mailto:\(nsString!.substring(with: range))", range: range) case let .TextUrl(url): - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.UrlAttribute, value: url, range: range) case .Bold: - string.addAttribute(NSFontAttributeName, value: boldFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSFontAttributeName, value: boldFont, range: range) case .Mention: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.TelegramPeerTextMentionAttribute, value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) string.addAttribute(TextNode.TelegramPeerMentionAttribute, value: peerId.toInt64() as NSNumber, range: range) case .Hashtag: if nsString == nil { @@ -73,13 +73,13 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(TextNode.TelegramHashtagAttribute, value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) } case .BotCommand: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.TelegramBotCommandAttribute, value: nsString!.substring(with: range), range: range) case .Code, .Pre: - string.addAttribute(NSFontAttributeName, value: fixedFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSFontAttributeName, value: fixedFont, range: range) default: break } diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift new file mode 100644 index 0000000000..ea045661a3 --- /dev/null +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -0,0 +1,67 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, media: Media, opportunistic: Bool) -> Signal { + switch media { + case let file as TelegramMediaFile: + let signal = Signal { subscriber in + let fetch = postbox.mediaBox.fetchedResource(file.resource).start() + let data = postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in + subscriber.putNext(next) + if next.complete { + subscriber.putCompletion() + } + }) + + return ActionDisposable { + fetch.dispose() + data.dispose() + } + } + + let result: Signal + if opportunistic { + result = signal |> take(1) + } else { + result = signal + } + + return result + |> mapToSignal { data -> Signal in + if data.complete { + if file.mimeType.hasPrefix("image/") { + return Signal { subscriber in + if let image = UIImage(contentsOfFile: data.path), let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), context: { size, context in + context.setBlendMode(.copy) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) { + let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) + + let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) + + subscriber.putNext(file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: scaledImageSize, resource: thumbnailResource)])) + subscriber.putCompletion() + } else { + subscriber.putNext(file.withUpdatedSize(data.size)) + subscriber.putCompletion() + } + + return EmptyDisposable + } |> runOn(opportunistic ? Queue.mainQueue() : Queue.concurrentDefaultQueue()) + } else { + return .single(file.withUpdatedSize(data.size)) + } + } else if opportunistic { + return .single(nil) + } else { + return .complete() + } + } + default: + return .single(nil) + } +} diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift new file mode 100644 index 0000000000..12e4e7ff5a --- /dev/null +++ b/TelegramUI/UserInfoController.swift @@ -0,0 +1,572 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class UserInfoControllerArguments { + let account: Account + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let changeNotificationMuteSettings: () -> Void + let openSharedMedia: () -> Void + let updatePeerBlocked: (Bool) -> Void + let deleteContact: () -> Void + + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) { + self.account = account + self.updateEditingName = updateEditingName + self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.openSharedMedia = openSharedMedia + self.updatePeerBlocked = updatePeerBlocked + self.deleteContact = deleteContact + } +} + +private enum UserInfoSection: ItemListSectionId { + case info + case actions + case sharedMediaAndNotifications + case block +} + +private enum UserInfoEntry: ItemListNodeEntry { + case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) + case about(text: String) + case phoneNumber(index: Int, value: PhoneNumberWithLabel) + case userName(value: String) + case sendMessage + case shareContact + case startSecretChat + case sharedMedia + case notifications(settings: PeerNotificationSettings?) + case notificationSound(settings: PeerNotificationSettings?) + case secretEncryptionKey(SecretChatKeyFingerprint) + case block(action: DestructiveUserInfoAction) + + var section: ItemListSectionId { + switch self { + case .info, .about, .phoneNumber, .userName: + return UserInfoSection.info.rawValue + case .sendMessage, .shareContact, .startSecretChat: + return UserInfoSection.actions.rawValue + case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey: + return UserInfoSection.sharedMediaAndNotifications.rawValue + case .block: + return UserInfoSection.block.rawValue + } + } + + var stableId: Int { + return self.sortIndex + } + + static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { + switch lhs { + case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState): + switch rhs { + case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState): + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer != nil) != (rhsPeer != nil) { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhsPresence != nil) != (rhsPresence != nil) { + return false + } + if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (lhsCachedData != nil) != (rhsCachedData != nil) { + return false + } + if lhsState != rhsState { + return false + } + return true + default: + return false + } + case let .about(lhsText): + switch rhs { + case .about(lhsText): + return true + default: + return false + } + case let .phoneNumber(lhsIndex, lhsValue): + switch rhs { + case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue: + return true + default: + return false + } + case let .userName(value): + switch rhs { + case .userName(value): + return true + default: + return false + } + case .sendMessage: + switch rhs { + case .sendMessage: + return true + default: + return false + } + case .shareContact: + switch rhs { + case .shareContact: + return true + default: + return false + } + case .startSecretChat: + switch rhs { + case .startSecretChat: + return true + default: + return false + } + case .sharedMedia: + switch rhs { + case .sharedMedia: + return true + default: + return false + } + case let .notifications(lhsSettings): + switch rhs { + case let .notifications(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case let .notificationSound(lhsSettings): + switch rhs { + case let .notificationSound(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case let .secretEncryptionKey(fingerprint): + if case .secretEncryptionKey(fingerprint) = rhs { + return true + } else { + return false + } + case let .block(action): + switch rhs { + case .block(action): + return true + default: + return false + } + } + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case .about: + return 1 + case let .phoneNumber(index, _): + return 2 + index + case .userName: + return 1000 + case .sendMessage: + return 1001 + case .shareContact: + return 1002 + case .startSecretChat: + return 1003 + case .sharedMedia: + return 1004 + case .notifications: + return 1005 + case .notificationSound: + return 1006 + case .secretEncryptionKey: + return 1007 + case .block: + return 1008 + } + } + + static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(_ arguments: UserInfoControllerArguments) -> ListViewItem { + switch self { + case let .info(peer, presence, cachedData, state): + return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }) + case let .about(text): + return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) + case let .phoneNumber(_, value): + return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section) + case let .userName(value): + return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section) + case .sendMessage: + return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + + }) + case .shareContact: + return ItemListActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + + }) + case .startSecretChat: + return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + + }) + case .sharedMedia: + return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { + arguments.openSharedMedia() + }) + 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: .plain, action: { + arguments.changeNotificationMuteSettings() + }) + case let .notificationSound(settings): + let label: String + label = "Default" + return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: { + }) + case let .secretEncryptionKey(fingerprint): + return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: { + }) + case let .block(action): + let title: String + switch action { + case .block: + title = "Block User" + case .unblock: + title = "Unblock User" + case .removeContact: + title = "Remove Contact" + } + return ItemListActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + switch action { + case .block: + arguments.updatePeerBlocked(true) + case .unblock: + arguments.updatePeerBlocked(false) + case .removeContact: + arguments.deleteContact() + } + }) + } + } +} + +private enum DestructiveUserInfoAction { + case block + case removeContact + case unblock +} + +private struct UserInfoEditingState: Equatable { + let editingName: ItemListAvatarAndNameInfoItemName? + + static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool { + if lhs.editingName != rhs.editingName { + return false + } + return true + } +} + +private struct UserInfoState: Equatable { + let savingData: Bool + let editingState: UserInfoEditingState? + + init() { + self.savingData = false + self.editingState = nil + } + + init(savingData: Bool, editingState: UserInfoEditingState?) { + self.savingData = savingData + self.editingState = editingState + } + + static func ==(lhs: UserInfoState, rhs: UserInfoState) -> Bool { + if lhs.savingData != rhs.savingData { + return false + } + if lhs.editingState != rhs.editingState { + return false + } + return true + } + + func withUpdatedSavingData(_ savingData: Bool) -> UserInfoState { + return UserInfoState(savingData: savingData, editingState: self.editingState) + } + + func withUpdatedEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState { + return UserInfoState(savingData: self.savingData, editingState: editingState) + } +} + +private func userInfoEntries(account: Account, view: PeerView, state: UserInfoState) -> [UserInfoEntry] { + var entries: [UserInfoEntry] = [] + + guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { + return [] + } + + var editingName: ItemListAvatarAndNameInfoItemName? + + var isEditing = false + if let editingState = state.editingState { + isEditing = true + + if view.peerIsContact { + editingName = editingState.editingName + } + } + + entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil))) + if let cachedUserData = view.cachedData as? CachedUserData { + if let about = cachedUserData.about, !about.isEmpty { + entries.append(UserInfoEntry.about(text: about)) + } + } + + var editable = true + if peer is TelegramSecretChat { + editable = false + } + + if let phoneNumber = user.phone, !phoneNumber.isEmpty { + entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) + } + + if !isEditing { + if let username = user.username, !username.isEmpty { + entries.append(UserInfoEntry.userName(value: username)) + } + + if !(peer is TelegramSecretChat) { + entries.append(UserInfoEntry.sendMessage) + if view.peerIsContact { + entries.append(UserInfoEntry.shareContact) + } + entries.append(UserInfoEntry.startSecretChat) + } + entries.append(UserInfoEntry.sharedMedia) + } + entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) + + if let _ = peer as? TelegramSecretChat { + entries.append(UserInfoEntry.secretEncryptionKey(SecretChatKeyFingerprint(k0: 0, k1: 0, k2: 0, k3: 0))) + } + + if isEditing { + entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings)) + if view.peerIsContact { + entries.append(UserInfoEntry.block(action: .removeContact)) + } + } else { + if let cachedData = view.cachedData as? CachedUserData { + if cachedData.isBlocked { + entries.append(UserInfoEntry.block(action: .unblock)) + } else { + entries.append(UserInfoEntry.block(action: .block)) + } + } + } + + return entries +} + +public func userInfoController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(UserInfoState(), ignoreRepeated: true) + let stateValue = Atomic(value: UserInfoState()) + let updateState: ((UserInfoState) -> UserInfoState) -> 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 updatePeerBlockedDisposable = MetaDisposable() + actionsDisposable.add(updatePeerBlockedDisposable) + + let changeMuteSettingsDisposable = MetaDisposable() + actionsDisposable.add(changeMuteSettingsDisposable) + + let arguments = UserInfoControllerArguments(account: account, updateEditingName: { editingName in + updateState { state in + if let editingState = state.editingState { + return state.withUpdatedEditingState(UserInfoEditingState(editingName: editingName)) + } else { + return state + } + } + }, changeNotificationMuteSettings: { + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let notificationAction: (Int32) -> Void = { muteUntil in + let muteState: PeerMuteState + if muteUntil <= 0 { + muteState = .unmuted + } else if muteUntil == Int32.max { + muteState = .muted(until: Int32.max) + } else { + muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + } + changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Enable", action: { + dismissAction() + notificationAction(0) + }), + ActionSheetButtonItem(title: "Mute for 1 hour", action: { + dismissAction() + notificationAction(1 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 8 hours", action: { + dismissAction() + notificationAction(8 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 2 days", action: { + dismissAction() + notificationAction(2 * 24 * 60 * 60) + }), + ActionSheetButtonItem(title: "Disable", action: { + dismissAction() + notificationAction(Int32.max) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, openSharedMedia: { + if let controller = peerSharedMediaController(account: account, peerId: peerId) { + pushControllerImpl?(controller) + } + }, updatePeerBlocked: { value in + updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) + }, deleteContact: { + + }) + + let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) + |> map { state, view -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in + let peer = peerViewMainPeer(view) + var leftNavigationButton: ItemListNavigationButton? + let rightNavigationButton: ItemListNavigationButton + if let editingState = state.editingState { + leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + updateState { + $0.withUpdatedEditingState(nil) + } + }) + + var doneEnabled = true + if let editingName = editingState.editingName, editingName.isEmpty { + doneEnabled = false + } + + if state.savingData { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: { + var updateName: ItemListAvatarAndNameInfoItemName? + updateState { state in + if let editingState = state.editingState, let editingName = editingState.editingName { + if let user = peer { + if ItemListAvatarAndNameInfoItemName(user.indexName) != editingName { + updateName = editingName + } + } + } + if updateName != nil { + return state.withUpdatedSavingData(true) + } else { + return state.withUpdatedEditingState(nil) + } + } + + if let updateName = updateName, case let .personName(firstName, lastName) = updateName { + updatePeerNameDisposable.set((updateContactName(account: account, peerId: peerId, firstName: firstName, lastName: lastName) |> 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 user = peer { + updateState { state in + return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user.indexName))) + } + } + }) + } + + let controllerState = ItemListControllerState(title: "Info", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) + let listState = ItemListNodeState(entries: userInfoEntries(account: account, view: view, state: state), style: .plain) + + 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/UserInfoEntries.swift b/TelegramUI/UserInfoEntries.swift index 14f0a20f4a..a2d18d4b73 100644 --- a/TelegramUI/UserInfoEntries.swift +++ b/TelegramUI/UserInfoEntries.swift @@ -4,367 +4,10 @@ import TelegramCore import SwiftSignalKit import Display -private enum UserInfoSection: ItemListSectionId { - case info - case actions - case sharedMediaAndNotifications - case block -} -enum DestructiveUserInfoAction { - case block - case removeContact -} -enum UserInfoEntry: PeerInfoEntry { - case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) - case about(text: String) - case phoneNumber(index: Int, value: PhoneNumberWithLabel) - case userName(value: String) - case sendMessage - case shareContact - case startSecretChat - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case notificationSound(settings: PeerNotificationSettings?) - case secretEncryptionKey(SecretChatKeyFingerprint) - case block(action: DestructiveUserInfoAction) +/*func userInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { - var section: ItemListSectionId { - switch self { - case .info, .about, .phoneNumber, .userName: - return UserInfoSection.info.rawValue - case .sendMessage, .shareContact, .startSecretChat: - return UserInfoSection.actions.rawValue - case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey: - return UserInfoSection.sharedMediaAndNotifications.rawValue - case .block: - return UserInfoSection.block.rawValue - } - } - - var stableId: PeerInfoEntryStableId { - return IntPeerInfoEntryStableId(value: self.sortIndex) - } - - func isEqual(to: PeerInfoEntry) -> Bool { - guard let entry = to as? UserInfoEntry else { - return false - } - - switch self { - case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState): - switch entry { - case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState): - if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else if (lhsPeer != nil) != (rhsPeer != nil) { - return false - } - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - if !lhsPresence.isEqual(to: rhsPresence) { - return false - } - } else if (lhsPresence != nil) != (rhsPresence != nil) { - return false - } - if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { - if !lhsCachedData.isEqual(to: rhsCachedData) { - return false - } - } else if (lhsCachedData != nil) != (rhsCachedData != nil) { - return false - } - if lhsState != rhsState { - return false - } - return true - default: - return false - } - case let .about(lhsText): - switch entry { - case let .about(lhsText): - return true - default: - return false - } - case let .phoneNumber(lhsIndex, lhsValue): - switch entry { - case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue: - return true - default: - return false - } - case let .userName(value): - switch entry { - case .userName(value): - return true - default: - return false - } - case .sendMessage: - switch entry { - case .sendMessage: - return true - default: - return false - } - case .shareContact: - switch entry { - case .shareContact: - return true - default: - return false - } - case .startSecretChat: - switch entry { - case .startSecretChat: - return true - default: - return false - } - case .sharedMedia: - switch entry { - case .sharedMedia: - return true - default: - return false - } - case let .notifications(lhsSettings): - switch entry { - case let .notifications(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case let .notificationSound(lhsSettings): - switch entry { - case let .notificationSound(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case let .secretEncryptionKey(fingerprint): - if case .secretEncryptionKey(fingerprint) = entry { - return true - } else { - return false - } - case let .block(action): - switch entry { - case .block(action): - return true - default: - return false - } - } - } - - private var sortIndex: Int { - switch self { - case .info: - return 0 - case .about: - return 1 - case let .phoneNumber(index, _): - return 2 + index - case .userName: - return 1000 - case .sendMessage: - return 1001 - case .shareContact: - return 1002 - case .startSecretChat: - return 1003 - case .sharedMedia: - return 1004 - case .notifications: - return 1005 - case .notificationSound: - return 1006 - case .secretEncryptionKey: - return 1007 - case .block: - return 1008 - } - } - - func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool { - guard let other = entry as? UserInfoEntry else { - return false - } - - return self.sortIndex < other.sortIndex - } - - func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { - switch self { - case let .info(peer, presence, cachedData, state): - return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in - - }) - case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) - case let .phoneNumber(_, value): - return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section) - case let .userName(value): - return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section) - case .sendMessage: - return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - case .shareContact: - return ItemListActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - case .startSecretChat: - return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - case .sharedMedia: - return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { - interaction.openSharedMedia() - }) - 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: .plain, action: { - interaction.changeNotificationMuteSettings() - }) - case let .notificationSound(settings): - let label: String - label = "Default" - return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: { - }) - case let .secretEncryptionKey(fingerprint): - return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: { - }) - case let .block(action): - let title: String - switch action { - case .block: - title = "Block User" - case .removeContact: - title = "Remove Contact" - } - return ItemListActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { - - }) - } - } -} - -struct UserInfoEditingState: Equatable { - let editingName: ItemListAvatarAndNameInfoItemName - - static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool { - if lhs.editingName != rhs.editingName { - return false - } - return true - } -} - -private final class UserInfoState: PeerInfoState { - fileprivate let editingState: UserInfoEditingState? - - init(editingState: UserInfoEditingState?) { - self.editingState = editingState - } - - func isEqual(to: PeerInfoState) -> Bool { - if let to = to as? UserInfoState { - return self.editingState == to.editingState - } else { - return false - } - } - - func updateEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState { - return UserInfoState(editingState: editingState) - } -} - -func userInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { - guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { - return PeerInfoEntries(entries: [], leftNavigationButton: nil, rightNavigationButton: nil) - } - - var entries: [PeerInfoEntry] = [] - - var editingName: ItemListAvatarAndNameInfoItemName? - var updatingName: ItemListAvatarAndNameInfoItemName? - - var isEditing = false - if let state = state as? UserInfoState, let editingState = state.editingState { - isEditing = true - - if view.peerIsContact { - editingName = editingState.editingName - } - } - - entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: updatingName))) - if let cachedUserData = view.cachedData as? CachedUserData { - if let about = cachedUserData.about, !about.isEmpty { - entries.append(UserInfoEntry.about(text: about)) - } - } - - var editable = true - if peer is TelegramSecretChat { - editable = false - } - - if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) - } - - if !isEditing { - if let username = user.username, !username.isEmpty { - entries.append(UserInfoEntry.userName(value: username)) - } - - if !(peer is TelegramSecretChat) { - entries.append(UserInfoEntry.sendMessage) - if view.peerIsContact { - entries.append(UserInfoEntry.shareContact) - } - entries.append(UserInfoEntry.startSecretChat) - } - entries.append(UserInfoEntry.sharedMedia) - } - entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) - - if let peer = peer as? TelegramSecretChat { - entries.append(UserInfoEntry.secretEncryptionKey(SecretChatKeyFingerprint(k0: 0, k1: 0, k2: 0, k3: 0))) - } - - if isEditing { - entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings)) - if view.peerIsContact { - entries.append(UserInfoEntry.block(action: .removeContact)) - } - } else { - entries.append(UserInfoEntry.block(action: .block)) - } var leftNavigationButton: PeerInfoNavigationButton? var rightNavigationButton: PeerInfoNavigationButton? @@ -408,4 +51,4 @@ func userInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries { } return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) -} +}*/ diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift new file mode 100644 index 0000000000..3acaf1d55e --- /dev/null +++ b/TelegramUI/UsernameSetupController.swift @@ -0,0 +1,300 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class UsernameSetupControllerArguments { + let account: Account + + let updatePublicLinkText: (String?, String) -> Void + + init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void) { + self.account = account + self.updatePublicLinkText = updatePublicLinkText + } +} + +private enum UsernameSetupSection: Int32 { + case link +} + +private enum UsernameSetupEntry: ItemListNodeEntry { + case editablePublicLink(String?, String) + case publicLinkStatus(String, AddressNameValidationStatus) + case publicLinkInfo(String) + + var section: ItemListSectionId { + switch self { + case .editablePublicLink, .publicLinkStatus, .publicLinkInfo: + return UsernameSetupSection.link.rawValue + } + } + + var stableId: Int32 { + switch self { + case .editablePublicLink: + return 0 + case .publicLinkStatus: + return 1 + case .publicLinkInfo: + return 2 + } + } + + static func ==(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool { + switch lhs { + case let .editablePublicLink(lhsCurrentText, lhsText): + if case let .editablePublicLink(rhsCurrentText, rhsText) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText { + 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 + } + } + } + + static func <(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: UsernameSetupControllerArguments) -> ListViewItem { + switch self { + case let .editablePublicLink(currentText, text): + return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in + arguments.updatePublicLinkText(currentText, updatedText) + }, action: { + + }) + 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 let .invalidFormat(error): + switch error { + case .startsWithDigit: + text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030)) + case .startsWithUnderscore: + text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(0xcf3030)) + case .endsWithUnderscore: + text = NSAttributedString(string: "Names can't end with an underscore.", textColor: UIColor(0xcf3030)) + case .tooShort: + text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030)) + case .invalidCharacters: + text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + } + case let .availability(availability): + switch availability { + case .available: + text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) + case .invalid: + text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + case .taken: + text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(0xcf3030)) + } + case .checking: + text = NSAttributedString(string: "Checking name...", textColor: UIColor(0x6d6d72)) + displayActivity = true + } + return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) + } + } +} + +private struct UsernameSetupControllerState: Equatable { + let editingPublicLinkText: String? + let addressNameValidationStatus: AddressNameValidationStatus? + let updatingAddressName: Bool + + init() { + self.editingPublicLinkText = nil + self.addressNameValidationStatus = nil + self.updatingAddressName = false + } + + init(editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool) { + self.editingPublicLinkText = editingPublicLinkText + self.addressNameValidationStatus = addressNameValidationStatus + self.updatingAddressName = updatingAddressName + } + + static func ==(lhs: UsernameSetupControllerState, rhs: UsernameSetupControllerState) -> Bool { + if lhs.editingPublicLinkText != rhs.editingPublicLinkText { + return false + } + if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus { + return false + } + if lhs.updatingAddressName != rhs.updatingAddressName { + return false + } + + return true + } + + func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> UsernameSetupControllerState { + return UsernameSetupControllerState(editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName) + } + + func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> UsernameSetupControllerState { + return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName) + } + + func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> UsernameSetupControllerState { + return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName) + } +} + +private func usernameSetupControllerEntries(view: PeerView, state: UsernameSetupControllerState) -> [UsernameSetupEntry] { + var entries: [UsernameSetupEntry] = [] + + if let peer = view.peers[view.peerId] as? TelegramUser { + let currentAddressName: String + if let current = state.editingPublicLinkText { + currentAddressName = current + } else { + if let addressName = peer.addressName { + currentAddressName = addressName + } else { + currentAddressName = "" + } + } + + entries.append(.editablePublicLink(peer.addressName, currentAddressName)) + if let status = state.addressNameValidationStatus { + entries.append(.publicLinkStatus(currentAddressName, status)) + } + entries.append(.publicLinkInfo("You can shoose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can user a-z, 0-9 and underscores. Minimum length is 5 characters.")) + } + + return entries +} + +public func usernameSetupController(account: Account) -> ViewController { + let statePromise = ValuePromise(UsernameSetupControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: UsernameSetupControllerState()) + let updateState: ((UsernameSetupControllerState) -> UsernameSetupControllerState) -> 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 = UsernameSetupControllerArguments(account: account, updatePublicLinkText: { currentText, text in + if text.isEmpty { + checkAddressNameDisposable.set(nil) + updateState { state in + return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil) + } + } else if currentText == text { + checkAddressNameDisposable.set(nil) + updateState { state in + return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil).withUpdatedAddressNameValidationStatus(nil) + } + } else { + updateState { state in + return state.withUpdatedEditingPublicLinkText(text) + } + + checkAddressNameDisposable.set((validateAddressNameInteractive(account: account, domain: .account, name: text) + |> deliverOnMainQueue).start(next: { result in + updateState { state in + return state.withUpdatedAddressNameValidationStatus(result) + } + })) + } + }) + + let peerView = account.viewTracker.peerView(account.peerId) + |> deliverOnMainQueue + + let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView) + |> map { state, view -> (ItemListControllerState, (ItemListNodeState, UsernameSetupEntry.ItemGenerationArguments)) in + let peer = peerViewMainPeer(view) + + var rightNavigationButton: ItemListNavigationButton? + if let peer = peer as? TelegramUser { + var doneEnabled = true + + if let addressNameValidationStatus = state.addressNameValidationStatus { + switch addressNameValidationStatus { + case .availability(.available): + break + default: + doneEnabled = false + } + } + + rightNavigationButton = ItemListNavigationButton(title: "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + var updatedAddressNameValue: String? + updateState { state in + if state.editingPublicLinkText != peer.addressName { + updatedAddressNameValue = state.editingPublicLinkText + } + + if updatedAddressNameValue != nil { + return state.withUpdatedUpdatingAddressName(true) + } else { + return state + } + } + + if let updatedAddressNameValue = updatedAddressNameValue { + updateAddressNameDisposable.set((updateAddressName(account: account, domain: .account, name: 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?() + } + }) + } + + let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let controllerState = ItemListControllerState(title: "Username", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) + let listState = ItemListNodeState(entries: usernameSetupControllerEntries(view: view, state: state), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + + return controller +} diff --git a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift new file mode 100644 index 0000000000..58da8c673d --- /dev/null +++ b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift @@ -0,0 +1,124 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) +private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() +}) + +final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { + private let webpageDisposable = MetaDisposable() + + private (set) var webpage: TelegramMediaWebpage + + let closeButton: ASButtonNode + let lineNode: ASImageNode + let titleNode: ASTextNode + let textNode: ASTextNode + + init(account: Account, webpage: TelegramMediaWebpage) { + self.webpage = webpage + + self.closeButton = ASButtonNode() + self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASImageNode() + self.lineNode.displayWithoutProcessing = true + self.lineNode.displaysAsynchronously = false + self.lineNode.image = lineImage + + self.titleNode = ASTextNode() + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.displaysAsynchronously = false + + self.textNode = ASTextNode() + self.textNode.truncationMode = .byTruncatingTail + self.textNode.maximumNumberOfLines = 1 + self.textNode.displaysAsynchronously = false + + super.init() + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.updateWebpage() + } + + deinit { + self.webpageDisposable.dispose() + } + + private func updateWebpage() { + var authorName = "" + var text = "" + switch self.webpage.content { + case .Pending: + authorName = "Loading..." + case let .Loaded(content): + if let title = content.title { + authorName = title + } else if let websiteName = content.websiteName { + authorName = websiteName + } else { + authorName = content.displayUrl + } + text = content.text ?? "" + } + + self.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: UIColor(0x007ee5)) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + + self.setNeedsLayout() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 45.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + let leftInset: CGFloat = 55.0 + let textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 55.0 + let textRightInset: CGFloat = 20.0 + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) + + let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) + } + + @objc func closePressed() { + if let dismiss = self.dismiss { + dismiss() + } + } +}