From 47cd4b0d76b818fd9752751ac4f642bc227c9f1e Mon Sep 17 00:00:00 2001 From: Peter Iakovlev Date: Wed, 31 Jan 2018 20:40:55 +0400 Subject: [PATCH] no message --- .gitignore | 4 + .gitmodules | 3 - .tx/config | 23 - Images.xcassets/Notification/Contents.json | 9 + .../SecretLock.imageset/Contents.json | 12 + .../SecretLock.imageset/ic_lock.pdf | Bin 0 -> 4133 bytes TelegramUI.xcodeproj/project.pbxproj | 116 ++- .../ArhivedStickerPacksController.swift | 4 +- .../AuthorizationSequenceController.swift | 28 +- ...ationSequencePasswordEntryController.swift | 2 +- TelegramUI/AuthorizationTheme.swift | 2 +- TelegramUI/BlockedPeersController.swift | 4 +- TelegramUI/BotCheckoutController.swift | 2 +- TelegramUI/BotCheckoutControllerNode.swift | 10 +- .../BotCheckoutInfoControllerNode.swift | 2 +- .../BotCheckoutPasswordEntryController.swift | 3 +- TelegramUI/BotReceiptController.swift | 2 +- TelegramUI/BotReceiptControllerNode.swift | 4 +- TelegramUI/CallListController.swift | 2 +- .../ChangePhoneNumberCodeController.swift | 18 +- TelegramUI/ChangePhoneNumberController.swift | 4 +- .../ChangePhoneNumberIntroController.swift | 2 +- TelegramUI/ChannelAdminController.swift | 76 +- TelegramUI/ChannelAdminsController.swift | 140 ++-- .../ChannelBannedMemberController.swift | 6 +- TelegramUI/ChannelBlacklistController.swift | 8 +- TelegramUI/ChannelInfoController.swift | 19 +- TelegramUI/ChannelMembersController.swift | 7 +- .../ChannelMembersSearchContainerNode.swift | 107 ++- .../ChannelMembersSearchController.swift | 4 +- .../ChannelMembersSearchControllerNode.swift | 4 +- TelegramUI/ChannelVisibilityController.swift | 8 +- TelegramUI/ChatBotInfoItem.swift | 5 +- TelegramUI/ChatBubbleVideoDecoration.swift | 32 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 2 +- TelegramUI/ChatController.swift | 496 ++++++++---- TelegramUI/ChatControllerBackground.swift | 35 + TelegramUI/ChatControllerInteraction.swift | 16 +- TelegramUI/ChatControllerNode.swift | 81 +- TelegramUI/ChatHistoryGridNode.swift | 2 +- TelegramUI/ChatHistoryListNode.swift | 52 +- TelegramUI/ChatHistoryViewForLocation.swift | 17 +- TelegramUI/ChatInfoTitlePanelNode.swift | 32 +- TelegramUI/ChatInterfaceInputContexts.swift | 152 ++-- TelegramUI/ChatInterfaceState.swift | 197 ++++- .../ChatInterfaceStateContextMenus.swift | 124 +-- .../ChatItemGalleryFooterContentNode.swift | 8 +- TelegramUI/ChatListController.swift | 2 +- TelegramUI/ChatListItem.swift | 31 +- TelegramUI/ChatListItemStrings.swift | 8 +- TelegramUI/ChatListNode.swift | 12 +- TelegramUI/ChatListNodeEntries.swift | 2 +- TelegramUI/ChatListSearchContainerNode.swift | 15 +- TelegramUI/ChatMessageActionItemNode.swift | 8 +- .../ChatMessageAttachedContentNode.swift | 3 +- TelegramUI/ChatMessageBubbleItemNode.swift | 31 +- .../ChatMessageContactBubbleContentNode.swift | 2 +- ...entLogPreviousDescriptionContentNode.swift | 109 +++ ...ssageEventLogPreviousLinkContentNode.swift | 105 +++ ...geEventLogPreviousMessageContentNode.swift | 110 +++ .../ChatMessageFileBubbleContentNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 10 +- .../ChatMessageInteractiveFileNode.swift | 5 + .../ChatMessageInteractiveMediaNode.swift | 4 +- TelegramUI/ChatMessageItem.swift | 10 +- .../ChatMessageMapBubbleContentNode.swift | 29 +- .../ChatMessageMediaBubbleContentNode.swift | 2 +- TelegramUI/ChatMessageNotificationItem.swift | 60 +- .../ChatMessageSelectionInputPanelNode.swift | 10 +- TelegramUI/ChatMessageStickerItemNode.swift | 6 +- .../ChatMessageTextBubbleContentNode.swift | 2 +- .../ChatMessageWebpageBubbleContentNode.swift | 2 +- .../ChatPanelInterfaceInteraction.swift | 4 +- TelegramUI/ChatPresentationData.swift | 2 + .../ChatPresentationInterfaceState.swift | 4 +- TelegramUI/ChatRecentActionsController.swift | 210 ++++++ .../ChatRecentActionsControllerNode.swift | 636 ++++++++++++++++ .../ChatRecentActionsControllerState.swift | 32 + TelegramUI/ChatRecentActionsEmptyNode.swift | 74 ++ .../ChatRecentActionsFilterController.swift | 468 ++++++++++++ .../ChatRecentActionsHistoryTransition.swift | 714 ++++++++++++++++++ TelegramUI/ChatRecentActionsInteraction.swift | 9 + ...ntActionsSearchNavigationContentNode.swift | 64 ++ TelegramUI/ChatRecentActionsTitleView.swift | 99 +++ TelegramUI/ChatSearchInputPanelNode.swift | 6 +- .../ChatSearchNavigationContentNode.swift | 22 +- TelegramUI/ChatTextInputAttributes.swift | 378 ++++++++++ .../ChatTextInputMediaRecordingButton.swift | 1 + TelegramUI/ChatTextInputMenu.swift | 69 ++ TelegramUI/ChatTextInputPanelNode.swift | 195 ++++- TelegramUI/ChatTitleView.swift | 6 +- .../CommandChatInputContextPanelNode.swift | 17 +- ...ntsThemes.swift => ComponentsThemes.swift} | 11 + TelegramUI/ComposeController.swift | 2 +- TelegramUI/ContactListNode.swift | 2 +- .../ContactMultiselectionController.swift | 2 +- TelegramUI/ContactsPeerItem.swift | 14 +- TelegramUI/ContactsSearchContainerNode.swift | 2 +- .../ConvertToSupergroupController.swift | 2 +- TelegramUI/CreateChannelController.swift | 4 +- TelegramUI/CreateGroupController.swift | 4 +- TelegramUI/DebugAccountsController.swift | 2 +- TelegramUI/EditSettingsController.swift | 11 +- TelegramUI/FeedGroupingController.swift | 53 ++ TelegramUI/FeedGroupingControllerNode.swift | 475 ++++++++++++ .../FetchPhotoLibraryImageResource.swift | 36 +- TelegramUI/GalleryController.swift | 67 +- TelegramUI/GenerateTextEntities.swift | 20 +- TelegramUI/GridMessageItem.swift | 14 +- TelegramUI/GroupAdminsController.swift | 21 +- TelegramUI/GroupInfoController.swift | 87 ++- TelegramUI/GroupInfoSearchItem.swift | 81 ++ ...GroupInfoSearchNavigationContentNode.swift | 65 ++ .../HashtagChatInputContextPanelNode.swift | 17 +- .../InstalledStickerPacksController.swift | 6 +- TelegramUI/InstantPageController.swift | 4 +- TelegramUI/InstantPageControllerNode.swift | 37 +- TelegramUI/InstantPageTextItem.swift | 28 +- TelegramUI/ItemListCheckboxItem.swift | 26 +- TelegramUI/ItemListController.swift | 147 +++- TelegramUI/ItemListControllerNode.swift | 80 +- TelegramUI/ItemListControllerSearch.swift | 27 + TelegramUI/ItemListMultilineTextItem.swift | 3 +- TelegramUI/ItemListPeerItem.swift | 78 +- TelegramUI/ItemListTextWithLabelItem.swift | 3 +- ...egacyPeerAvatarPlaceholderDataSource.swift | 4 +- TelegramUI/ListMessageFileItemNode.swift | 4 +- TelegramUI/ListMessageSnippetItemNode.swift | 8 +- TelegramUI/LiveLocationManager.swift | 66 +- ...MediaNavigationAccessoryItemListNode.swift | 8 +- TelegramUI/MediaPlayerScrubbingNode.swift | 137 ++-- .../MentionChatInputContextPanelNode.swift | 27 +- TelegramUI/MergeLists.swift | 333 ++++++++ TelegramUI/NativeVideoContent.swift | 4 +- TelegramUI/NavigateToChatController.swift | 2 +- TelegramUI/NotificationSoundSelection.swift | 10 +- TelegramUI/OpenChatMessage.swift | 17 +- TelegramUI/OpenResolvedUrl.swift | 27 + TelegramUI/OpenUrl.swift | 20 +- TelegramUI/OverlayPlayerController.swift | 4 + TelegramUI/OverlayPlayerControllerNode.swift | 13 +- .../PeerMediaCollectionController.swift | 24 +- TelegramUI/PeerSelectionControllerNode.swift | 2 +- TelegramUI/PerformanceSpinner.swift | 41 - TelegramUI/PhotoResources.swift | 81 +- TelegramUI/PresentationData.swift | 17 +- TelegramUI/PresentationResourceKey.swift | 2 + TelegramUI/PresentationResourcesChat.swift | 12 + .../PresentationResourcesRootController.swift | 6 + TelegramUI/PrivacyAndSecurityController.swift | 2 +- TelegramUI/ProxySettingsController.swift | 8 +- TelegramUI/RecentSessionsController.swift | 8 +- .../SearchDisplayControllerContentNode.swift | 2 - .../SelectivePrivacySettingsController.swift | 12 +- ...ectivePrivacySettingsPeersController.swift | 4 +- TelegramUI/SettingsController.swift | 7 +- TelegramUI/StorageUsageController.swift | 2 +- TelegramUI/StringWithAppliedEntities.swift | 48 +- TelegramUI/TelegramApplicationContext.swift | 38 +- TelegramUI/TelegramController.swift | 2 +- .../TelegramInitializeLegacyComponents.swift | 4 - TelegramUI/ThemeSettingsChatPreviewItem.swift | 1 + TelegramUI/ThemeSettingsController.swift | 2 +- .../TransformOutgoingMessageMedia.swift | 15 +- ...pVerificationPasswordEntryController.swift | 15 +- .../TwoStepVerificationResetController.swift | 12 +- .../TwoStepVerificationUnlockController.swift | 18 +- TelegramUI/UniversalVideoCalleryItem.swift | 2 +- TelegramUI/UserInfoController.swift | 29 +- TelegramUI/UsernameSetupController.swift | 4 +- ...textResultsChatInputContextPanelNode.swift | 1 + .../VoiceCallDataSavingController.swift | 6 +- TelegramUI/WebEmbedVideoContent.swift | 7 + submodules/libtgvoip | 1 - 174 files changed, 6849 insertions(+), 1177 deletions(-) create mode 100644 .gitignore delete mode 100644 .gitmodules delete mode 100644 .tx/config create mode 100644 Images.xcassets/Notification/Contents.json create mode 100644 Images.xcassets/Notification/SecretLock.imageset/Contents.json create mode 100644 Images.xcassets/Notification/SecretLock.imageset/ic_lock.pdf create mode 100644 TelegramUI/ChatControllerBackground.swift create mode 100644 TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift create mode 100644 TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift create mode 100644 TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift create mode 100644 TelegramUI/ChatRecentActionsController.swift create mode 100644 TelegramUI/ChatRecentActionsControllerNode.swift create mode 100644 TelegramUI/ChatRecentActionsControllerState.swift create mode 100644 TelegramUI/ChatRecentActionsEmptyNode.swift create mode 100644 TelegramUI/ChatRecentActionsFilterController.swift create mode 100644 TelegramUI/ChatRecentActionsHistoryTransition.swift create mode 100644 TelegramUI/ChatRecentActionsInteraction.swift create mode 100644 TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift create mode 100644 TelegramUI/ChatRecentActionsTitleView.swift create mode 100644 TelegramUI/ChatTextInputAttributes.swift create mode 100644 TelegramUI/ChatTextInputMenu.swift rename TelegramUI/{CompomentsThemes.swift => ComponentsThemes.swift} (68%) create mode 100644 TelegramUI/FeedGroupingController.swift create mode 100644 TelegramUI/FeedGroupingControllerNode.swift create mode 100644 TelegramUI/GroupInfoSearchItem.swift create mode 100644 TelegramUI/GroupInfoSearchNavigationContentNode.swift create mode 100644 TelegramUI/ItemListControllerSearch.swift create mode 100644 TelegramUI/MergeLists.swift create mode 100644 TelegramUI/OpenResolvedUrl.swift delete mode 100644 TelegramUI/PerformanceSpinner.swift delete mode 160000 submodules/libtgvoip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..a6256f9605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.dSYM +*.dSYM.zip +*.ipa +*/xcuserdata/* diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f86d21f5c9..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "submodules/libtgvoip"] - path = submodules/libtgvoip - url = https://bitbucket.org/grishka/libtgvoip.git diff --git a/.tx/config b/.tx/config deleted file mode 100644 index 9fbe69c828..0000000000 --- a/.tx/config +++ /dev/null @@ -1,23 +0,0 @@ -[main] -host = https://www.transifex.com - -[iphone-1.descriptiontxt] -file_filter = translations/iphone-1.descriptiontxt/.txt -source_lang = en -type = TXT - -[iphone-1.infopliststrings] -file_filter = translations/iphone-1.infopliststrings/.strings -source_lang = en -type = STRINGS - -[iphone-1.localizablestrings_1] -file_filter = translations/iphone-1.localizablestrings_1/.strings -source_lang = en -type = STRINGS - -[iphone-1.localizablestrings] -file_filter = translations/iphone-1.localizablestrings/.strings -source_lang = en -type = STRINGS - diff --git a/Images.xcassets/Notification/Contents.json b/Images.xcassets/Notification/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Notification/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Notification/SecretLock.imageset/Contents.json b/Images.xcassets/Notification/SecretLock.imageset/Contents.json new file mode 100644 index 0000000000..d40bb4f3c6 --- /dev/null +++ b/Images.xcassets/Notification/SecretLock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lock.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Notification/SecretLock.imageset/ic_lock.pdf b/Images.xcassets/Notification/SecretLock.imageset/ic_lock.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1946fe633aca3d3151ae68144f7728aeafe45556 GIT binary patch literal 4133 zcmai%cT^Kw*TyMPARvNL1W`sVRa!z4AS%5C1Pw)y&=Ux~Ne4lSluMH?f+EtJ6a@u= z(4`2Mh%^xa>C&rIxqOM`yWZD#t#{U}nKRFvvrpO2AHU6~rK~Cn6@!8KT4y$97V|fs zylrg*!vP53jI#q@xdMo5;+?GBZ2$yG(gDO(Z5`b4uB5jE)*Y{mw{W(^1G2JUH+NS& z))DMY>8R6ny%oYxHzzzBUCHk`Hg_02aDF$C7No;Qx81lpRe9~XTBS_uinjxLmY&_O zNtc4&Y3ePLx}TW7?y?=Ky5r~hhNx1+U+=>HaD$Nlxv;%OD|@i zB$xfdC3kaiWD$>DV%=nz<``SoCDK#_uHLtCOTJYSBfSog9(5~|ZUb&xF36m`Q?Uy#LYpoqdN*Ujw ztQal%3ygQN{Bi807CBqw6p`OBNjUi@xhe5WB5(M(-~n+hto662E8fW+fPGJ@4&Ke# z!_@-s21tGvl$@R1N&npdvJpsGzlOK8#VR^`14a;%fCQitCSX$5e@sFi^wTX`uFe*^ zcz3{vG?TI_U;>CMIXgJJ>bhVp@BrCPN^Vd<>RVJ!KBsJq^+znGa7AAw_1YO97{WF$BYlp zRSgYWulf(wP-uM(1~t1IPzJ&?4Omtdn~YDL?rPnkI>SWCXh7LGVPyqE?J86Ce#sLzSdk@tQ;82=rzvSx6!b zD#4aXm-Uoqs|8yoNriY^r{y4f;Yb=d#B0CzCY5aiilXbQq4W4jA zJ&j^!N3gGN_mL(AgeBFidyYTTAwoihg<~aRgNlMSsTEo0${4kWw0TV7oo78A35rn1 zTD5tl+nn(9{O2N`*RXqNQ6yo=#?OYI5A6b zNWj2Ez!^ya8jd^Z_AE9YW)%LRNUdshnhs|f3Z*}&d7b2Zsr(m>n4sW}`Z}AE_%gRx z3E#ix_PObeBb_ER6?k-Kw;5tBUHP&^II_n>9Ga+3#nHGw0mFmDAXFmeC%6J(F4SWH zMM$6;6ZL&+F*Pb%dC+qO+4mG+1?YQ9&%1Um6#YTIZ$a0CC0&jigKjpSaG_@iM9G80 znlvDkV?nmG!D~#cSqepQY&j=W6$hEl8q)eGDQHQvP`_4+h+|5S-+RknaOUFOqD&<% znXNzEIcsQ5?(RRKc?-agA5!{=S3#C`3ZdniFZ|J7}qURSjs*t}83X8SqjvU$Bqo$)PG#NsD95Ir}O`E&kRt z_3`jb?1M9GZ5&hKnCJdoD6D(*iI2SkYEYCpTshlUBB=f@< z(N=x)SaYNDlxNiMu}hb^7~2~;Cb?YhRG%@Ha6*)Vs!k3?*9I=OoS5KW_pZA*#=pd0FTJSO}y{DcdMotG?8u4gdvrsz`M48V-< zWP+Slk{~||`YM--;_!9k)mQR9@;*wrIwuO8`VER6)TA4t3CjLg{l!s(zK=DRQ*j@;s7@$(@1oO1 zzhs_dt7OXL*dk3m=K+?1^diS1bU(z7-wtaxY3S)mHN*|X&C0Y_Gi9|i zN-}a9Pz^=_ZyMhMAIzf^9yS$ZMrJl(A7is8EdD5pFYdjVZJ2$zP1@q+t;luRDI@;! zh;;P?^_b3Uy6^kPZkX2)D~SyDvo`$jVnl6wR{2K^=Beu76J*Pk4!>-lhkL^GGEoLm z>1?BHW+KZXx}u9B+!!IuF$2fSh4THD53OnTCKmqfVRlm%6+L;QvoA^tMaMlGmi;fM znUs&R)n2b9ZZmH49Pl4NX=K8^!wHN#@7+BGYeW18m=`}!RQIe$Ptge67I@aDo4C2 zb?CW2x;eBqw?A}%21$dS(tQT!fK);HG_v#(bf=DafLfX;n`(o(#ow9n8uznNDSlP* z481~!U@&3bcRM2{0)NWCi)Vc{i#NKD9Ino>~!u8Q-s1`r!Ivrm%}x z8fVsoxpndUS?+V=Oh*x?n+s<&OSjBkdCh=^5^KW`66V^3=VI1PcJW{1K`9~6Lwc1{ zT1_G>)JxS_)u*V?3NJo+JU812nl4O=OUf3474{bXWgAktTiK_`hm9;3Hi5|q5X4*H z*$}0gH?{*SdZ)AGd*d|)-7r?Q5AOY~`9&?7_aWb141I;&^D%1_KI4scH}mY)blS92 z*SXHk&VxG$D}za(5MDu-2 zSRBXr-`pGR-VRwN=tF6ZqQK6{Z_U2fRfy@0vQUy{;lFcy*ZO^7| zwL2ev`l59Ebn(0vEc?|iNcsYDeX@O*d-XBHm0_Pq|Dt(;!;I~hZw(_2GY3BoM!d|C zw7so)dwf2yFa2E4IZ?TXM}Aw;+o@?4Z>%+l@ja}M9wIZm?rbQwg|0_Mu!mo|=`LGS zIsGoZw7hhzD=4MRbp4fU;Rxa2t6oa6bt%&1vVXciVL9wDX7GINZA zV)S10sPD6_*K1wTDScC+4MKmv+)3^&&bZ@sK(NMjwyxRtE~d&|9V@6u?hkG|&RKq* z?(?!;&C8D*E3TK@4m!*{u-IykpI<`nqLVXX@@ju}0_Fqw&%*Y-j8ixYn8n%{h zBz7iaJ1N*Nd<-Rn_rC)wVcoF~&eq?N-t7m=|3lPJ=#LSC*RU1S)NiHIsEr0#~XQke*cq=djfWp9#|Gxl91OkQttbiXH zsiqxgFqxm+xia;E=fAk|IiT9ziBX-^zZXZ!b#cw z_gENA^0$64IP$l(z!32Nq9K3Thr27*)&cMOt@hTn^(CDjss7P%b|(3Q>;ck2sykUZ zlg#qHtxU4Z6)7AZfxsd05G%YS4i1IlEi4hza4Zrb0fi%=l2BRj|L*eLZf@=*YkW%x PObRLq=HpXAtAhUz61fyn literal 0 HcmV?d00001 diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index be0a3ce12b..02dceae807 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -58,6 +58,16 @@ D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */; }; D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */; }; D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; + D04281ED200E3B28009DDE36 /* ItemListControllerSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */; }; + D04281EF200E3D88009DDE36 /* GroupInfoSearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */; }; + D04281F1200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */; }; + D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F3200E5AB0009DDE36 /* ChatRecentActionsController.swift */; }; + D04281F6200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F5200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift */; }; + D04281F8200E5C17009DDE36 /* ChatControllerBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F7200E5C17009DDE36 /* ChatControllerBackground.swift */; }; + D04281FA200E5CDC009DDE36 /* ChatRecentActionsControllerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F9200E5CDC009DDE36 /* ChatRecentActionsControllerState.swift */; }; + D04281FC200E61BC009DDE36 /* ChatRecentActionsInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281FB200E61BC009DDE36 /* ChatRecentActionsInteraction.swift */; }; + D04281FE200E639A009DDE36 /* ChatRecentActionsTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281FD200E639A009DDE36 /* ChatRecentActionsTitleView.swift */; }; + D0428200200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281FF200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift */; }; D0430B001FF4570500A35ADD /* WebController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430AFF1FF4570500A35ADD /* WebController.swift */; }; D0430B021FF4584100A35ADD /* WebControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0430B011FF4584100A35ADD /* WebControllerNode.swift */; }; D046142E2004DB3700EC0EF2 /* LiveLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D046142D2004DB3700EC0EF2 /* LiveLocationManager.swift */; }; @@ -92,6 +102,8 @@ D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; }; D04ECD721FFBF22B00DE9029 /* OpenUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */; }; D053B4371F1A9CA000E2D58A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D053B4361F1A9CA000E2D58A /* WebKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D053DADA201A4C4400993D32 /* ChatTextInputAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053DAD9201A4C4400993D32 /* ChatTextInputAttributes.swift */; }; + D053DADC201AAAB100993D32 /* ChatTextInputMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053DADB201AAAB100993D32 /* ChatTextInputMenu.swift */; }; D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */; }; D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */; }; D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D056CD6F1FF147B000880D28 /* IconButtonNode.swift */; }; @@ -210,6 +222,13 @@ D0DE66061F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */; }; D0DFD5E21FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */; }; D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */; }; + D0E817472010E62F00B82BBB /* MergeLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E817462010E62E00B82BBB /* MergeLists.swift */; }; + D0E8174C2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8174B2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift */; }; + D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8174D2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift */; }; + D0E817502012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8174F2012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift */; }; + D0E8175720122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175620122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift */; }; + D0E8175920122FE100B82BBB /* ChatRecentActionsFilterController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175820122FE100B82BBB /* ChatRecentActionsFilterController.swift */; }; + D0E8175B201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175A201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift */; }; D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; }; D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; }; D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */; }; @@ -444,7 +463,6 @@ D0EC6CCD1EB9F58800EBF1C3 /* DeclareEncodables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE701DCBF23F007511FD /* DeclareEncodables.swift */; }; D0EC6CCE1EB9F58800EBF1C3 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B66B71DD672D00049C3D2 /* GeoLocation.swift */; }; - D0EC6CD01EB9F58800EBF1C3 /* PerformanceSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00219031DDCC86400BE708A /* PerformanceSpinner.swift */; }; D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023836F1DDF0462004018B6 /* UrlHandling.swift */; }; D0EC6CD21EB9F58800EBF1C3 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; }; D0EC6CD31EB9F58800EBF1C3 /* GenerateTextEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */; }; @@ -458,7 +476,7 @@ D0EC6CDB1EB9F58800EBF1C3 /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C2AAC1E768404001F6F9A /* Markdown.swift */; }; D0EC6CDC1EB9F58800EBF1C3 /* TelegramAccountAuxiliaryMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F3A8AA1E82D83E00B4C64C /* TelegramAccountAuxiliaryMethods.swift */; }; D0EC6CDD1EB9F58800EBF1C3 /* PresentationCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B3E1EB8F3E500EBF1C3 /* PresentationCallManager.swift */; }; - D0EC6CDE1EB9F58800EBF1C3 /* CompomentsThemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174C51EAE58FC00A1BF36 /* CompomentsThemes.swift */; }; + D0EC6CDE1EB9F58800EBF1C3 /* ComponentsThemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174C51EAE58FC00A1BF36 /* ComponentsThemes.swift */; }; D0EC6CDF1EB9F58800EBF1C3 /* PresentationResourceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05BFB5E1EAA22F900909D38 /* PresentationResourceKey.swift */; }; D0EC6CE01EB9F58800EBF1C3 /* PresentationResourcesRootController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174BD1EAE161C00A1BF36 /* PresentationResourcesRootController.swift */; }; D0EC6CE11EB9F58800EBF1C3 /* PresentationResourcesItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05174BB1EAE156500A1BF36 /* PresentationResourcesItemList.swift */; }; @@ -990,9 +1008,12 @@ D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */; }; D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; }; D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; }; + D0F8C397201774A200236FC5 /* FeedGroupingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F8C396201774A200236FC5 /* FeedGroupingController.swift */; }; + D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */; }; D0F9720F1FFE4BD5002595C8 /* notification.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0C50E431E93FCD200F62E39 /* notification.caf */; }; D0F972101FFE4BD5002595C8 /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; D0FB87B21F7C4C19004DE005 /* FetchMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */; }; + D0FC194D201F82A000FEDBB2 /* OpenResolvedUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */; }; D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */; }; @@ -1005,7 +1026,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - D00219031DDCC86400BE708A /* PerformanceSpinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceSpinner.swift; sourceTree = ""; }; D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = ""; }; D002A0D01E9B99F500A81812 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = ""; }; D002A0D21E9BBE6700A81812 /* MultiplexedSoftwareVideoSourceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplexedSoftwareVideoSourceManager.swift; sourceTree = ""; }; @@ -1171,6 +1191,16 @@ 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 = ""; }; + D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListControllerSearch.swift; sourceTree = ""; }; + D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoSearchItem.swift; sourceTree = ""; }; + D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoSearchNavigationContentNode.swift; sourceTree = ""; }; + D04281F3200E5AB0009DDE36 /* ChatRecentActionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsController.swift; sourceTree = ""; }; + D04281F5200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsControllerNode.swift; sourceTree = ""; }; + D04281F7200E5C17009DDE36 /* ChatControllerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatControllerBackground.swift; sourceTree = ""; }; + D04281F9200E5CDC009DDE36 /* ChatRecentActionsControllerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsControllerState.swift; sourceTree = ""; }; + D04281FB200E61BC009DDE36 /* ChatRecentActionsInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsInteraction.swift; sourceTree = ""; }; + D04281FD200E639A009DDE36 /* ChatRecentActionsTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsTitleView.swift; sourceTree = ""; }; + D04281FF200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsHistoryTransition.swift; sourceTree = ""; }; D042C6801E8D9A6700C863B0 /* GalleryFooterNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryFooterNode.swift; sourceTree = ""; }; D042C6851E8DA69D00C863B0 /* GalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryFooterContentNode.swift; sourceTree = ""; }; D042C6871E8DA8C800C863B0 /* GalleryControllerPresentationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryControllerPresentationState.swift; sourceTree = ""; }; @@ -1296,13 +1326,15 @@ D05174BD1EAE161C00A1BF36 /* PresentationResourcesRootController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesRootController.swift; sourceTree = ""; }; D05174BF1EAE3AD400A1BF36 /* DefaultDarkPresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultDarkPresentationTheme.swift; sourceTree = ""; }; D05174C21EAE583800A1BF36 /* TelegramRootController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramRootController.swift; sourceTree = ""; }; - D05174C51EAE58FC00A1BF36 /* CompomentsThemes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompomentsThemes.swift; sourceTree = ""; }; + D05174C51EAE58FC00A1BF36 /* ComponentsThemes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComponentsThemes.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 = ""; }; D053B4361F1A9CA000E2D58A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + D053DAD9201A4C4400993D32 /* ChatTextInputAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputAttributes.swift; sourceTree = ""; }; + D053DADB201AAAB100993D32 /* ChatTextInputMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputMenu.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 = ""; }; @@ -1619,6 +1651,13 @@ D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryViewForLocation.swift; sourceTree = ""; }; D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntriesForView.swift; sourceTree = ""; }; D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedChatHistoryViewTransition.swift; sourceTree = ""; }; + D0E817462010E62E00B82BBB /* MergeLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MergeLists.swift; sourceTree = ""; }; + D0E8174B2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEventLogPreviousMessageContentNode.swift; sourceTree = ""; }; + D0E8174D2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEventLogPreviousDescriptionContentNode.swift; sourceTree = ""; }; + D0E8174F2012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEventLogPreviousLinkContentNode.swift; sourceTree = ""; }; + D0E8175620122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsSearchNavigationContentNode.swift; sourceTree = ""; }; + D0E8175820122FE100B82BBB /* ChatRecentActionsFilterController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsFilterController.swift; sourceTree = ""; }; + D0E8175A201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsEmptyNode.swift; sourceTree = ""; }; D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentDisclosureItemNode.swift; sourceTree = ""; }; D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = PhoneCountries.txt; path = TelegramUI/Resources/PhoneCountries.txt; sourceTree = ""; }; D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPaymentMethodSheet.swift; sourceTree = ""; }; @@ -2201,6 +2240,8 @@ D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = ""; }; D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleImages.swift; sourceTree = ""; }; D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDateHeader.swift; sourceTree = ""; }; + D0F8C396201774A200236FC5 /* FeedGroupingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGroupingController.swift; sourceTree = ""; }; + D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGroupingControllerNode.swift; sourceTree = ""; }; D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateTextEntities.swift; sourceTree = ""; }; D0FA0ABE1E76E17F005BB9B7 /* TwoStepVerificationPasswordEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationPasswordEntryController.swift; sourceTree = ""; }; D0FA0AC01E7725AA005BB9B7 /* TwoStepVerificationResetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationResetController.swift; sourceTree = ""; }; @@ -2208,6 +2249,7 @@ D0FA34FE1EA5834C00E56FFA /* ItemListControllerSegmentedTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerSegmentedTitleView.swift; sourceTree = ""; }; D0FA35001EA6127000E56FFA /* StorageUsageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageUsageController.swift; sourceTree = ""; }; D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchMediaUtils.swift; sourceTree = ""; }; + D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenResolvedUrl.swift; sourceTree = ""; }; D0FC40821D5B8E7400261D9D /* TelegramUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramUI.h; sourceTree = ""; }; D0FC40831D5B8E7400261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0FC40881D5B8E7500261D9D /* TelegramUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TelegramUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2337,6 +2379,7 @@ D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */, D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */, D0FA34FE1EA5834C00E56FFA /* ItemListControllerSegmentedTitleView.swift */, + D04281EC200E3B28009DDE36 /* ItemListControllerSearch.swift */, ); name = "Item List"; sourceTree = ""; @@ -2468,6 +2511,22 @@ name = "Accessory Panels"; sourceTree = ""; }; + D04281F2200E5A70009DDE36 /* Chat Recent Actions */ = { + isa = PBXGroup; + children = ( + D04281F3200E5AB0009DDE36 /* ChatRecentActionsController.swift */, + D04281F5200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift */, + D04281F9200E5CDC009DDE36 /* ChatRecentActionsControllerState.swift */, + D04281FB200E61BC009DDE36 /* ChatRecentActionsInteraction.swift */, + D04281FD200E639A009DDE36 /* ChatRecentActionsTitleView.swift */, + D04281FF200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift */, + D0E8175620122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift */, + D0E8175820122FE100B82BBB /* ChatRecentActionsFilterController.swift */, + D0E8175A201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift */, + ); + name = "Chat Recent Actions"; + sourceTree = ""; + }; D0430AFE1FF456F400A35ADD /* Web */ = { isa = PBXGroup; children = ( @@ -2717,7 +2776,7 @@ D05174C41EAE58E900A1BF36 /* Utils */ = { isa = PBXGroup; children = ( - D05174C51EAE58FC00A1BF36 /* CompomentsThemes.swift */, + D05174C51EAE58FC00A1BF36 /* ComponentsThemes.swift */, ); name = Utils; sourceTree = ""; @@ -4242,6 +4301,8 @@ D0F67FEF1EE6B8A8000E5906 /* ChannelMembersSearchController.swift */, D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */, D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */, + D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */, + D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -4402,6 +4463,7 @@ D0F69DF61D6B8A720046BCD6 /* Chat List */, D017494F1E1067C00057C89A /* Hashtag Search */, D0F69E0D1D6B8AB90046BCD6 /* Chat */, + D04281F2200E5A70009DDE36 /* Chat Recent Actions */, D00DE6961E8E8E21003F0D76 /* Share */, D0F69E4E1D6B8BB90046BCD6 /* Media */, D0F69E6C1D6B8C220046BCD6 /* Contacts */, @@ -4416,6 +4478,7 @@ D0F69E791D6B8C3B0046BCD6 /* Settings */, D0C50E361E93CAF200F62E39 /* Notifications */, D0430AFE1FF456F400A35ADD /* Web */, + D0F8C3952017747300236FC5 /* Feed */, ); name = Controllers; sourceTree = ""; @@ -4476,6 +4539,7 @@ D0F69E0E1D6B8ACF0046BCD6 /* ChatController.swift */, D0F69E0F1D6B8ACF0046BCD6 /* ChatControllerInteraction.swift */, D0F69E101D6B8ACF0046BCD6 /* ChatControllerNode.swift */, + D04281F7200E5C17009DDE36 /* ChatControllerBackground.swift */, D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */, D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */, D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */, @@ -4545,6 +4609,9 @@ D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */, D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */, D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */, + D0E8174B2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift */, + D0E8174D2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift */, + D0E8174F2012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift */, ); name = Items; sourceTree = ""; @@ -4559,6 +4626,8 @@ D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */, D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */, D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */, + D053DAD9201A4C4400993D32 /* ChatTextInputAttributes.swift */, + D053DADB201AAAB100993D32 /* ChatTextInputMenu.swift */, ); name = "Text Input"; sourceTree = ""; @@ -4694,6 +4763,7 @@ D0F69E911D6B8C8E0046BCD6 /* Utils */ = { isa = PBXGroup; children = ( + D0E817462010E62E00B82BBB /* MergeLists.swift */, D04614352005093B00EC0EF2 /* Device Location */, D025A4241F79428300563950 /* Fetch Manager */, D046142C2004DB1D00EC0EF2 /* Live Location Manager */, @@ -4707,7 +4777,6 @@ D073CE701DCBF23F007511FD /* DeclareEncodables.swift */, D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */, D04B66B71DD672D00049C3D2 /* GeoLocation.swift */, - D00219031DDCC86400BE708A /* PerformanceSpinner.swift */, D023836F1DDF0462004018B6 /* UrlHandling.swift */, D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */, D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */, @@ -4734,6 +4803,7 @@ D018477D1FFBC01E00075256 /* TimestampStrings.swift */, D0C26D561FDF2388004ABF18 /* OpenChatMessage.swift */, D04ECD711FFBF22B00DE9029 /* OpenUrl.swift */, + D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */, ); name = Utils; sourceTree = ""; @@ -4758,6 +4828,15 @@ name = Resources; sourceTree = ""; }; + D0F8C3952017747300236FC5 /* Feed */ = { + isa = PBXGroup; + children = ( + D0F8C396201774A200236FC5 /* FeedGroupingController.swift */, + D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */, + ); + name = Feed; + sourceTree = ""; + }; D0FA0AC21E7742CE005BB9B7 /* Privacy and Security */ = { isa = PBXGroup; children = ( @@ -5060,6 +5139,7 @@ D0F0AAE21EC20EF8005EE2A5 /* CallControllerStatusNode.swift in Sources */, D0EC6CB41EB9F58800EBF1C3 /* timing.c in Sources */, D0EC6CB51EB9F58800EBF1C3 /* platform_log.c in Sources */, + D04281F6200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift in Sources */, D0EC6CB61EB9F58800EBF1C3 /* RMGeometry.m in Sources */, D079FCDD1F05C4F20038FADE /* LocalAuth.swift in Sources */, D0EC6CB71EB9F58800EBF1C3 /* RMIntroPageView.m in Sources */, @@ -5119,7 +5199,6 @@ D0EC6FB41EBA114200EBF1C3 /* aec_resampler.cc in Sources */, D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */, D0EC6EA71EBA0FB000EBF1C3 /* BlockingQueue.cpp in Sources */, - D0EC6CD01EB9F58800EBF1C3 /* PerformanceSpinner.swift in Sources */, D09394132007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift in Sources */, D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */, D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */, @@ -5146,7 +5225,7 @@ D0EC6FDF1EBA135100EBF1C3 /* resample_by_2_internal.c in Sources */, D0EC6CDD1EB9F58800EBF1C3 /* PresentationCallManager.swift in Sources */, D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */, - D0EC6CDE1EB9F58800EBF1C3 /* CompomentsThemes.swift in Sources */, + D0EC6CDE1EB9F58800EBF1C3 /* ComponentsThemes.swift in Sources */, D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */, D01C06BC1FBBB0D8001561AB /* CheckNode.swift in Sources */, D0EC6CDF1EB9F58800EBF1C3 /* PresentationResourceKey.swift in Sources */, @@ -5182,6 +5261,7 @@ D0EC6CF01EB9F58800EBF1C3 /* AutomaticMediaDownloadSettings.swift in Sources */, D0EC6CF11EB9F58800EBF1C3 /* GeneratedMediaStoreSettings.swift in Sources */, D0EC6CF21EB9F58800EBF1C3 /* VoiceCallSettings.swift in Sources */, + D0F8C397201774A200236FC5 /* FeedGroupingController.swift in Sources */, D0EC6CF31EB9F58800EBF1C3 /* PresentationThemeSettings.swift in Sources */, D0EC6EB51EBA0FC100EBF1C3 /* Resampler.cpp in Sources */, D0EC6CF41EB9F58800EBF1C3 /* ManagedMediaId.swift in Sources */, @@ -5205,6 +5285,7 @@ D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */, D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */, D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */, + D0E817502012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift in Sources */, D0EC6CFC1EB9F58800EBF1C3 /* ManagedAudioPlaylistPlayer.swift in Sources */, D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */, D0EC6CFE1EB9F58800EBF1C3 /* PeerMediaAudioPlaylist.swift in Sources */, @@ -5245,6 +5326,7 @@ D0EC6D191EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContext.swift in Sources */, D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */, D0EC6FF61EBA195F00EBF1C3 /* TGLogWrapper.m in Sources */, + D053DADC201AAAB100993D32 /* ChatTextInputMenu.swift in Sources */, D0EC6EAD1EBA0FBB00EBF1C3 /* JitterBuffer.cpp in Sources */, D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0EC6D1B1EB9F58800EBF1C3 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, @@ -5281,6 +5363,7 @@ D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */, D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */, D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */, + D04281EF200E3D88009DDE36 /* GroupInfoSearchItem.swift in Sources */, D0EC6FA51EBA111500EBF1C3 /* audio_util.cc in Sources */, D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */, D0EC6FFD1EBA1F2400EBF1C3 /* OngoingCallThreadLocalContext.mm in Sources */, @@ -5308,6 +5391,7 @@ D0EC6D351EB9F58800EBF1C3 /* SearchBarNode.swift in Sources */, D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, D0EC6D371EB9F58800EBF1C3 /* SearchDisplayController.swift in Sources */, + D04281ED200E3B28009DDE36 /* ItemListControllerSearch.swift in Sources */, D0EC6D381EB9F58800EBF1C3 /* SearchDisplayControllerContentNode.swift in Sources */, D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */, D0EC6D391EB9F58800EBF1C3 /* ImageContainingNode.swift in Sources */, @@ -5346,6 +5430,7 @@ D0EC6D551EB9F58800EBF1C3 /* PreparedChatHistoryViewTransition.swift in Sources */, D0EB41FB1F30E75000838FE6 /* LegacyImageDownloadActor.swift in Sources */, D0208ADA1FA34017001F0D5F /* DeviceProximityManager.m in Sources */, + D04281FC200E61BC009DDE36 /* ChatRecentActionsInteraction.swift in Sources */, D0EC6D561EB9F58800EBF1C3 /* ChatHistoryNode.swift in Sources */, D0EC6FAF1EBA112600EBF1C3 /* delay_estimator_wrapper.cc in Sources */, D0EC6D571EB9F58800EBF1C3 /* ChatHistoryListNode.swift in Sources */, @@ -5461,6 +5546,8 @@ D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */, D0EC6D961EB9F58900EBF1C3 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0EC6D971EB9F58900EBF1C3 /* ChatMessageItem.swift in Sources */, + D0E8175720122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift in Sources */, + D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, D0430B001FF4570500A35ADD /* WebController.swift in Sources */, @@ -5554,6 +5641,7 @@ D0EC6DCC1EB9F58900EBF1C3 /* ChatButtonKeyboardInputNode.swift in Sources */, D0CFBB861FD715E700B65C0D /* LegacyHTTPOperationImpl.swift in Sources */, D0EC6DCD1EB9F58900EBF1C3 /* ChatInputContextPanelNode.swift in Sources */, + D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */, D0EC6DCE1EB9F58900EBF1C3 /* HorizontalStickersChatContextPanelNode.swift in Sources */, D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */, D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */, @@ -5565,8 +5653,11 @@ D0EC6DD61EB9F58900EBF1C3 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */, D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, + D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */, D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */, D020A9DA1FEAE675008C66F7 /* OverlayPlayerController.swift in Sources */, + D0E817472010E62F00B82BBB /* MergeLists.swift in Sources */, + D0E8174C2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift in Sources */, D0EC6DD91EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */, D0E9BA161F05574500F079A4 /* STPCardValidator.m in Sources */, D0EC6DDA1EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, @@ -5586,6 +5677,7 @@ D09D88711F86D36700BEB4C9 /* CountryList.swift in Sources */, D0EC6FAE1EBA112600EBF1C3 /* delay_estimator.cc in Sources */, D0EC6DE11EB9F58900EBF1C3 /* ChatMessageSelectionInputPanelNode.swift in Sources */, + D04281FA200E5CDC009DDE36 /* ChatRecentActionsControllerState.swift in Sources */, D0EC6DE21EB9F58900EBF1C3 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, D0EC6DE31EB9F58900EBF1C3 /* ChatBotStartInputPanelNode.swift in Sources */, D0EC6DE41EB9F58900EBF1C3 /* ChatUnblockInputPanelNode.swift in Sources */, @@ -5593,6 +5685,7 @@ D06BEC771F62F68B0035A545 /* OverlayUniversalVideoNode.swift in Sources */, D0EC6DE61EB9F58900EBF1C3 /* DeleteChatInputPanelNode.swift in Sources */, D01BAA241ECE173200295217 /* PresentationResourcesCallList.swift in Sources */, + D0428200200E6A00009DDE36 /* ChatRecentActionsHistoryTransition.swift in Sources */, D0EC6DE71EB9F58900EBF1C3 /* ChatTitleAccessoryPanelNode.swift in Sources */, D0EC6DE81EB9F58900EBF1C3 /* ChatPinnedMessageTitlePanelNode.swift in Sources */, D0EC6FBF1EBA132B00EBF1C3 /* nsx_core.c in Sources */, @@ -5675,6 +5768,7 @@ D0EC6E1B1EB9F58900EBF1C3 /* InstantPageImageNode.swift in Sources */, D0EC6E1C1EB9F58900EBF1C3 /* InstantPageWebEmbedItem.swift in Sources */, D0EC6E1D1EB9F58900EBF1C3 /* InstantPageWebEmbedNode.swift in Sources */, + D04281F1200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift in Sources */, D0EC6E1E1EB9F58900EBF1C3 /* InstantPageShapeItem.swift in Sources */, D0EC6E1F1EB9F58900EBF1C3 /* InstantPageTile.swift in Sources */, D0EC6E201EB9F58900EBF1C3 /* InstantPageTileNode.swift in Sources */, @@ -5683,6 +5777,7 @@ D0EC6EAE1EBA0FBB00EBF1C3 /* logging.cpp in Sources */, D0EC6E231EB9F58900EBF1C3 /* StickerPackPreviewController.swift in Sources */, D0EC6E241EB9F58900EBF1C3 /* StickerPackPreviewControllerNode.swift in Sources */, + D0FC194D201F82A000FEDBB2 /* OpenResolvedUrl.swift in Sources */, D0EC6E251EB9F58900EBF1C3 /* StickerPackPreviewGridItem.swift in Sources */, D0EC6E261EB9F58900EBF1C3 /* StickerPreviewController.swift in Sources */, D0EC6E271EB9F58900EBF1C3 /* StickerPreviewControllerNode.swift in Sources */, @@ -5726,6 +5821,7 @@ D0EC6E401EB9F58900EBF1C3 /* ItemListActivityTextItem.swift in Sources */, D0EC6E411EB9F58900EBF1C3 /* ItemListEditableItem.swift in Sources */, D0EC6E421EB9F58900EBF1C3 /* ItemListRevealOptionsNode.swift in Sources */, + D0E8175920122FE100B82BBB /* ChatRecentActionsFilterController.swift in Sources */, D0EC6FC91EBA135100EBF1C3 /* cross_correlation_neon.c in Sources */, D0EC6E431EB9F58900EBF1C3 /* ItemListEditableDeleteControlNode.swift in Sources */, D0EC6E441EB9F58900EBF1C3 /* ItemListSingleLineInputItem.swift in Sources */, @@ -5766,6 +5862,7 @@ D0EC6E5B1EB9F58900EBF1C3 /* CallController.swift in Sources */, D0EC6E5C1EB9F58900EBF1C3 /* CallControllerNode.swift in Sources */, D0EC6E5D1EB9F58900EBF1C3 /* PrivacyAndSecurityController.swift in Sources */, + D04281F8200E5C17009DDE36 /* ChatControllerBackground.swift in Sources */, D0EC6E5E1EB9F58900EBF1C3 /* ItemListRecentSessionItem.swift in Sources */, D0EC6FCE1EBA135100EBF1C3 /* energy.c in Sources */, D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */, @@ -5821,6 +5918,7 @@ D0EC6E7C1EB9F58900EBF1C3 /* UsernameSetupController.swift in Sources */, D0471B621EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift in Sources */, D09250041FE5363D003F693F /* ExperimentalSettings.swift in Sources */, + D0E8175B201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift in Sources */, D0C44B641FC64D0500227BE0 /* SwipeToDismissGestureRecognizer.swift in Sources */, D0EC6E7D1EB9F58900EBF1C3 /* ChangePhoneNumberIntroController.swift in Sources */, D0EC6FA31EBA10E400EBF1C3 /* checks.cc in Sources */, @@ -5830,12 +5928,14 @@ D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */, D0EC6E811EB9F58900EBF1C3 /* NotificationContainerController.swift in Sources */, D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */, + D053DADA201A4C4400993D32 /* ChatTextInputAttributes.swift in Sources */, D0EC6E821EB9F58900EBF1C3 /* NotificationContainerControllerNode.swift in Sources */, D0EC6E831EB9F58900EBF1C3 /* NotificationItemContainerNode.swift in Sources */, D0EC6E841EB9F58900EBF1C3 /* NotificationItem.swift in Sources */, D0EC6E851EB9F58900EBF1C3 /* ChatMessageNotificationItem.swift in Sources */, D0EC6E861EB9F58900EBF1C3 /* UIImage+WebP.m in Sources */, D0EC6E871EB9F58900EBF1C3 /* FastBlur.m in Sources */, + D04281FE200E639A009DDE36 /* ChatRecentActionsTitleView.swift in Sources */, D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */, D046142E2004DB3700EC0EF2 /* LiveLocationManager.swift in Sources */, D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */, diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 4f75dc5444..349da325d2 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -302,13 +302,13 @@ public func archivedStickerPacksController(account: Account) -> ViewController { var rightNavigationButton: ItemListNavigationButton? if let packs = packs, packs.count != 0 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditing(true) } diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 85e46fc920..7d66b4cc31 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -16,7 +16,7 @@ public final class AuthorizationSequenceController: NavigationController { private let apiId: Int32 private let apiHash: String private let strings: PresentationStrings - private let theme: AuthorizationTheme + public let theme: AuthorizationTheme private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() @@ -108,7 +108,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -150,7 +150,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -171,7 +171,7 @@ public final class AuthorizationSequenceController: NavigationController { controller?.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) } else { - controller?.present(standardTextAlertController(title: nil, text: strongSelf.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller?.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } else { controller?.inProgress = true @@ -196,7 +196,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -238,7 +238,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -260,7 +260,7 @@ public final class AuthorizationSequenceController: NavigationController { } }).start() case .none: - strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) strongController.didForgotWithNoRecovery = true } } @@ -273,7 +273,7 @@ public final class AuthorizationSequenceController: NavigationController { } controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [ + strongController.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { @@ -291,7 +291,7 @@ public final class AuthorizationSequenceController: NavigationController { case .generic: text = strongSelf.strings.Login_UnknownError } - strongController.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -334,7 +334,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_CodeExpiredError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -342,7 +342,7 @@ public final class AuthorizationSequenceController: NavigationController { } controller.noAccess = { [weak self, weak controller] in if let strongSelf = self, let controller = controller { - controller.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) let account = strongSelf.account let _ = (strongSelf.account.postbox.modify { modifier -> Void in if let state = modifier.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _) = state.contents { @@ -371,7 +371,7 @@ public final class AuthorizationSequenceController: NavigationController { controller = AuthorizationSequenceAwaitingAccountResetController(strings: self.strings, theme: self.theme) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(title: nil, text: strongSelf.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ + strongController.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { @@ -389,7 +389,7 @@ public final class AuthorizationSequenceController: NavigationController { case .generic: text = strongSelf.strings.Login_UnknownError } - strongController.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -445,7 +445,7 @@ public final class AuthorizationSequenceController: NavigationController { text = strongSelf.strings.Login_UnknownError } - controller.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) } } })) diff --git a/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/TelegramUI/AuthorizationSequencePasswordEntryController.swift index 0722e37335..84ab845b92 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -109,7 +109,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { func forgotPressed() { if self.didForgotWithNoRecovery { - self.present(standardTextAlertController(title: nil, text: self.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: self.theme), title: nil, text: self.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) } else { self.forgot?() } diff --git a/TelegramUI/AuthorizationTheme.swift b/TelegramUI/AuthorizationTheme.swift index 2dcd50ccf4..ba7d570a0f 100644 --- a/TelegramUI/AuthorizationTheme.swift +++ b/TelegramUI/AuthorizationTheme.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import Display -final class AuthorizationTheme { +public final class AuthorizationTheme { let statusBarStyle: StatusBarStyle let navigationBarBackgroundColor: UIColor let navigationBarTextColor: UIColor diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index 097f8c8dc7..a0c9c08251 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -249,13 +249,13 @@ public func blockedPeersController(account: Account) -> ViewController { var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } diff --git a/TelegramUI/BotCheckoutController.swift b/TelegramUI/BotCheckoutController.swift index 1ee661b269..5cf9760dd4 100644 --- a/TelegramUI/BotCheckoutController.swift +++ b/TelegramUI/BotCheckoutController.swift @@ -48,7 +48,7 @@ final class BotCheckoutController: ViewController { } override func loadDisplayNode() { - let displayNode = BotCheckoutControllerNode(updateNavigationOffset: { [weak self] offset in + let displayNode = BotCheckoutControllerNode(navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in if let strongSelf = self { strongSelf.navigationOffset = offset } diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index 4fb4a51ab0..3b96f8a975 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -324,7 +324,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, private var applePayAuthrorizationCompletion: ((PKPaymentAuthorizationStatus) -> Void)? private var applePayController: PKPaymentAuthorizationViewController? - init(updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) { + init(navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) { self.account = account self.messageId = messageId self.present = present @@ -359,7 +359,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, self.inProgressDimNode.isUserInteractionEnabled = false self.inProgressDimNode.backgroundColor = UIColor.white.withAlphaComponent(0.5) - super.init(updateNavigationOffset: updateNavigationOffset, state: signal) + super.init(navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) self.arguments = arguments @@ -666,7 +666,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } else { - strongSelf.present(standardTextAlertController(title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle, providerPeer.displayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle, providerPeer.displayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotPaymentLiability(postbox: strongSelf.account.postbox, peerId: strongSelf.messageId.peerId).start() strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) @@ -734,7 +734,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, text = strongSelf.presentationData.strings.Checkout_ErrorGeneric } - strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } })) } @@ -761,7 +761,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeout(durationString).0 } - strongSelf.present(standardTextAlertController(title: nil, text: alertText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: alertText, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_No, action: { if let strongSelf = self { strongSelf.pay(savedCredentialsToken: token) diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift index 585d3919cf..8a50c23052 100644 --- a/TelegramUI/BotCheckoutInfoControllerNode.swift +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -301,7 +301,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi text = strongSelf.strings.Login_UnknownError } - strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) } })) diff --git a/TelegramUI/BotCheckoutPasswordEntryController.swift b/TelegramUI/BotCheckoutPasswordEntryController.swift index c8b48f92c9..e090ccc5fc 100644 --- a/TelegramUI/BotCheckoutPasswordEntryController.swift +++ b/TelegramUI/BotCheckoutPasswordEntryController.swift @@ -296,7 +296,8 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { func botCheckoutPasswordEntryController(account: Account, strings: PresentationStrings, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { var dismissImpl: (() -> Void)? - let controller = AlertController(contentNode: BotCheckoutPasswordAlertContentNode(account: account, strings: strings, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = AlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), contentNode: BotCheckoutPasswordAlertContentNode(account: account, strings: strings, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { dismissImpl?() }, completion: { token in completion(token) diff --git a/TelegramUI/BotReceiptController.swift b/TelegramUI/BotReceiptController.swift index 6ee2fd7100..5445be7f70 100644 --- a/TelegramUI/BotReceiptController.swift +++ b/TelegramUI/BotReceiptController.swift @@ -46,7 +46,7 @@ final class BotReceiptController: ViewController { } override func loadDisplayNode() { - let displayNode = BotReceiptControllerNode(updateNavigationOffset: { [weak self] offset in + let displayNode = BotReceiptControllerNode(navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in if let strongSelf = self { strongSelf.navigationOffset = offset } diff --git a/TelegramUI/BotReceiptControllerNode.swift b/TelegramUI/BotReceiptControllerNode.swift index f295fa95f5..467a029530 100644 --- a/TelegramUI/BotReceiptControllerNode.swift +++ b/TelegramUI/BotReceiptControllerNode.swift @@ -259,7 +259,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { private let actionButton: BotCheckoutActionButton - init(updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) { + init(navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, account: Account, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) { self.account = account self.dismissAnimated = dismissAnimated @@ -277,7 +277,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) self.actionButton.setState(.inactive(self.presentationData.strings.Common_Done)) - super.init(updateNavigationOffset: updateNavigationOffset, state: signal) + super.init(navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) self.dataRequestDisposable = (requestBotPaymentReceipt(network: account.network, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in if let strongSelf = self { diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index 86fa293a6c..949068d375 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -251,7 +251,7 @@ public final class CallListController: ViewController { return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { [weak self] peer, current in if let strongSelf = self, let peer = peer, let current = current { - strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { if let strongSelf = self { let _ = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) began?() diff --git a/TelegramUI/ChangePhoneNumberCodeController.swift b/TelegramUI/ChangePhoneNumberCodeController.swift index d9deb0deb2..c30073739c 100644 --- a/TelegramUI/ChangePhoneNumberCodeController.swift +++ b/TelegramUI/ChangePhoneNumberCodeController.swift @@ -223,23 +223,25 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code updateState { return $0.withUpdatedChecking(false) } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let alertText: String switch error { case .generic: - alertText = "An error occurred." + alertText = presentationData.strings.Login_UnknownError case .invalidCode: - alertText = "Invalid code. Please try again." + alertText = presentationData.strings.Login_InvalidCodeError case .codeExpired: - alertText = "Code expired." + alertText = presentationData.strings.Login_CodeExpiredError case .limitExceeded: - alertText = "You have entered invalid code too many times. Please try again later." + alertText = presentationData.strings.Login_CodeFloodError } - presentControllerImpl?(standardTextAlertController(title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, completed: { updateState { return $0.withUpdatedChecking(false) } - presentControllerImpl?(standardTextAlertController(title: nil, text: "You have changed your phone number to \(formatPhoneNumber(phoneNumber)).", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "You have changed your phone number to \(formatPhoneNumber(phoneNumber)).", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) dismissImpl?() })) } @@ -265,13 +267,13 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code |> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { var nextEnabled = true if state.codeText.isEmpty { nextEnabled = false } - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { checkCode() }) } diff --git a/TelegramUI/ChangePhoneNumberController.swift b/TelegramUI/ChangePhoneNumberController.swift index a9a11dd03e..c1e216c25b 100644 --- a/TelegramUI/ChangePhoneNumberController.swift +++ b/TelegramUI/ChangePhoneNumberController.swift @@ -109,6 +109,8 @@ final class ChangePhoneNumberController: ViewController { }, error: { [weak self] error in if let strongSelf = self { strongSelf.inProgress = false + + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } let text: String switch error { @@ -122,7 +124,7 @@ final class ChangePhoneNumberController: ViewController { text = "An error occurred. Please try again later." } - strongSelf.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } })) } else { diff --git a/TelegramUI/ChangePhoneNumberIntroController.swift b/TelegramUI/ChangePhoneNumberIntroController.swift index 68019aa1ad..bbec7d0f9f 100644 --- a/TelegramUI/ChangePhoneNumberIntroController.swift +++ b/TelegramUI/ChangePhoneNumberIntroController.swift @@ -130,7 +130,7 @@ final class ChangePhoneNumberIntroController: ViewController { } func proceed() { - self.present(standardTextAlertController(title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.presentationData.theme), title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(account: strongSelf.account), animated: true) } diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index 084c78be24..e963d80272 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -24,15 +24,21 @@ private enum ChannelAdminSection: Int32 { private enum ChannelAdminEntryStableId: Hashable { case info + case rightsTitle case right(TelegramChannelAdminRightsFlags) + case addAdminsInfo case dismiss var hashValue: Int { switch self { case .info: return 0 - case .dismiss: + case .rightsTitle: return 1 + case .addAdminsInfo: + return 2 + case .dismiss: + return 3 case let .right(flags): return flags.rawValue.hashValue } @@ -46,12 +52,24 @@ private enum ChannelAdminEntryStableId: Hashable { } else { return false } + case .rightsTitle: + if case .rightsTitle = rhs { + return true + } else { + return false + } case let right(flags): if case .right(flags) = rhs { return true } else { return false } + case .addAdminsInfo: + if case .addAdminsInfo = rhs { + return true + } else { + return false + } case .dismiss: if case .dismiss = rhs { return true @@ -64,14 +82,16 @@ private enum ChannelAdminEntryStableId: Hashable { private enum ChannelAdminEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, Peer, TelegramUserPresence?) + case rightsTitle(PresentationTheme, String) case rightItem(PresentationTheme, Int, String, TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags, Bool, Bool) + case addAdminsInfo(PresentationTheme, String) case dismiss(PresentationTheme, String) var section: ItemListSectionId { switch self { case .info: return ChannelAdminSection.info.rawValue - case .rightItem: + case .rightsTitle, .rightItem, .addAdminsInfo: return ChannelAdminSection.rights.rawValue case .dismiss: return ChannelAdminSection.dismiss.rawValue @@ -82,8 +102,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { switch self { case .info: return .info + case .rightsTitle: + return .rightsTitle case let .rightItem(_, _, _, right, _, _, _): return .right(right) + case .addAdminsInfo: + return .addAdminsInfo case .dismiss: return .dismiss } @@ -106,6 +130,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return false } + return true + } else { + return false + } + case let .rightsTitle(lhsTheme, lhsText): + if case let .rightsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -137,6 +167,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } else { return false } + case let .addAdminsInfo(lhsTheme, lhsText): + if case let .addAdminsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .dismiss(lhsTheme, lhsText): if case let .dismiss(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -155,15 +191,29 @@ private enum ChannelAdminEntry: ItemListNodeEntry { default: return true } + case .rightsTitle: + switch rhs { + case .info, .rightsTitle: + return false + default: + return true + } case let .rightItem(_, lhsIndex, _, _, _, _, _): switch rhs { - case .info: + case .info, .rightsTitle: return false case let .rightItem(_, rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex default: return true } + case .addAdminsInfo: + switch rhs { + case .info, .rightsTitle, .rightItem, .addAdminsInfo: + return false + default: + return true + } case .dismiss: return false } @@ -175,10 +225,14 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in }, avatarTapped: { }) + case let .rightsTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .rightItem(theme, _, text, right, flags, value, enabled): return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in arguments.toggleRight(right, flags) }) + case let .addAdminsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .dismiss(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.dismissAdmin() @@ -292,6 +346,8 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] { entries.append(.info(presentationData.theme, presentationData.strings, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence)) + entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) + let isGroup: Bool let maskRightsFlags: TelegramChannelAdminRightsFlags let rightsOrder: [TelegramChannelAdminRightsFlags] @@ -314,8 +370,6 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .canChangeInfo, .canDeleteMessages, .canBanUsers, - .canInviteUsers, - .canChangeInviteLink, .canPinMessages, .canAddAdmins ] @@ -347,6 +401,10 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s index += 1 } } + + if accountUserRightsFlags.contains(.canAddAdmins) { + entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) + } if let initialParticipant = initialParticipant { var canDismiss = false @@ -427,20 +485,20 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe let leftNavigationButton: ItemListNavigationButton if canEdit { - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } else { - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { dismissImpl?() }) } var rightNavigationButton: ItemListNavigationButton? if state.updating { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else if canEdit { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { if let _ = initialParticipant { var updateFlags: TelegramChannelAdminRightsFlags? updateState { current in diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 50c1890310..b6fe654896 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -7,14 +7,16 @@ import TelegramCore private final class ChannelAdminsControllerArguments { let account: Account + let openRecentActions: () -> Void let updateCurrentAdministrationType: () -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removeAdmin: (PeerId) -> Void let addAdmin: () -> Void let openAdmin: (ChannelParticipant) -> Void - init(account: Account, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { + init(account: Account, openRecentActions: @escaping () -> Void, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { self.account = account + self.openRecentActions = openRecentActions self.updateCurrentAdministrationType = updateCurrentAdministrationType self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removeAdmin = removeAdmin @@ -60,6 +62,7 @@ private enum ChannelAdminsEntryStableId: Hashable { } private enum ChannelAdminsEntry: ItemListNodeEntry { + case recentActions(PresentationTheme, String) case administrationType(PresentationTheme, String, String) case administrationInfo(PresentationTheme, String) @@ -70,7 +73,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .administrationType, .administrationInfo: + case .recentActions, .administrationType, .administrationInfo: return ChannelAdminsSection.administration.rawValue case .adminsHeader, .adminPeerItem, .addAdmin, .adminsInfo: return ChannelAdminsSection.admins.rawValue @@ -79,16 +82,18 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { var stableId: ChannelAdminsEntryStableId { switch self { - case .administrationType: + case .recentActions: return .index(0) - case .administrationInfo: + case .administrationType: return .index(1) - case .adminsHeader: + case .administrationInfo: return .index(2) - case .addAdmin: + case .adminsHeader: return .index(3) - case .adminsInfo: + case .addAdmin: return .index(4) + case .adminsInfo: + return .index(5) case let .adminPeerItem(_, _, _, _, participant, _, _): return .peer(participant.peer.id) } @@ -96,6 +101,12 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { static func ==(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { switch lhs { + case let .recentActions(lhsTheme, lhsText): + if case let .recentActions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .administrationType(lhsTheme, lhsText, lhsValue): if case let .administrationType(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -158,25 +169,32 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { static func <(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { switch lhs { - case .administrationType: + case .recentActions: return true + case .administrationType: + switch rhs { + case .recentActions: + return false + default: + return true + } case .administrationInfo: switch rhs { - case .administrationType: + case .recentActions, .administrationType: return false default: return true } case .adminsHeader: switch rhs { - case .administrationType, .administrationInfo: + case .recentActions, .administrationType, .administrationInfo: return false default: return true } case let .adminPeerItem(_, _, _, index, _, _, _): switch rhs { - case .administrationType, .administrationInfo, .adminsHeader: + case .recentActions, .administrationType, .administrationInfo, .adminsHeader: return false case let .adminPeerItem(_, _, _, rhsIndex, _, _, _): return index < rhsIndex @@ -185,7 +203,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } case .addAdmin: switch rhs { - case .administrationType, .administrationInfo, .adminsHeader, .adminPeerItem: + case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .adminPeerItem: return false default: return true @@ -197,6 +215,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { func item(_ arguments: ChannelAdminsControllerArguments) -> ListViewItem { switch self { + case let .recentActions(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + arguments.openRecentActions() + }) case let .administrationType(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.updateCurrentAdministrationType() @@ -214,15 +236,11 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { action = nil case let .member(_, _, adminInfo, _): if let adminInfo = adminInfo { - let baseFlags: TelegramChannelAdminRightsFlags - if isGroup { - baseFlags = .groupSpecific + if let peer = participant.peers[adminInfo.promotedBy] { + peerText = strings.Channel_Management_PromotedBy(peer.displayTitle).0 } else { - baseFlags = .broadcastSpecific + peerText = "" } - let flags = adminInfo.rights.flags.intersection(baseFlags) - peerText = "" - //peerText = strings.Channel_Management_LabelRights(Int32(flags.count)) } else { peerText = "" } @@ -332,6 +350,8 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, if case let .group(info) = peer.info { isGroup = true + entries.append(.recentActions(presentationData.theme, presentationData.strings.Group_Info_AdminLog)) + if peer.flags.contains(.isCreator) { let selectedType: CurrentAdministrationType if let current = state.selectedType { @@ -358,6 +378,8 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, entries.append(.administrationInfo(presentationData.theme, infoText)) } + } else { + entries.append(.recentActions(presentationData.theme, presentationData.strings.Group_Info_AdminLog)) } if let participants = participants { @@ -433,6 +455,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon statePromise.set(stateValue.modify { f($0) }) } + var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? let actionsDisposable = DisposableSet() @@ -450,7 +473,12 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData - let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: { + let arguments = ChannelAdminsControllerArguments(account: account, openRecentActions: { + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + pushControllerImpl?(ChatRecentActionsController(account: account, peer: peer)) + }) + }, updateCurrentAdministrationType: { let _ = (presentationDataSignal |> take(1) |> deliverOnMainQueue).start(next: { presentationData in let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) let result = ValuePromise() @@ -551,38 +579,42 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let applyAdmin: Signal = adminsPromise.get() |> filter { $0 != nil } |> take(1) - |> deliverOnMainQueue |> mapToSignal { admins -> Signal in - if let admins = admins { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - var updatedAdmins = admins - if updatedRights.isEmpty { - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == peer.id { - updatedAdmins.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == peer.id { - if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: [:]) + return account.postbox.loadedPeerWithId(account.peerId) + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { accountPeer in + if let admins = admins { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var updatedAdmins = admins + if updatedRights.isEmpty { + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + updatedAdmins.remove(at: i) + break } - found = true - break + } + } else { + var found = false + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: updatedAdmins[i].peers) + } + found = true + break + } + } + if !found { + updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer, peers: [accountPeer.id: accountPeer])) } } - if !found { - updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer, peers: [:])) - } + adminsPromise.set(.single(updatedAdmins)) } - adminsPromise.set(.single(updatedAdmins)) + + return .complete() } - - return .complete() } addAdminDisposable.set(applyAdmin.start()) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -609,7 +641,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon for i in 0 ..< updatedAdmins.count { if updatedAdmins[i].peer.id == adminId { if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: [:]) + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: updatedAdmins[i].peers) } found = true break @@ -643,13 +675,13 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon var rightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else if let peer = view.peers[peerId] as? TelegramChannel, peer.flags.contains(.isCreator) { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -660,7 +692,12 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let previous = previousPeers previousPeers = admins - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: nil, animateChanges: true) + var isGroup = true + if let peer = view.peers[peerId] as? TelegramChannel, case .broadcast = peer.info { + isGroup = false + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, view: view, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) @@ -669,6 +706,9 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } let controller = ItemListController(account: account, state: signal) + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) diff --git a/TelegramUI/ChannelBannedMemberController.swift b/TelegramUI/ChannelBannedMemberController.swift index 15da51cd01..901358e49a 100644 --- a/TelegramUI/ChannelBannedMemberController.swift +++ b/TelegramUI/ChannelBannedMemberController.swift @@ -337,15 +337,15 @@ public func channelBannedMemberController(account: Account, peerId: PeerId, memb let memberView = combinedView.views[.peer(peerId: memberId)] as! PeerView let leftNavigationButton: ItemListNavigationButton - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.updating { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { /*if let _ = initialParticipant { var updateFlags: TelegramChannelBannedMemberRightsFlags? updateState { current in diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index 60ce38adaf..29110c2c29 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -329,9 +329,9 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View }))*/ }, openPeer: { participant in - presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { rights in + /*presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { rights in - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/ }) let peerView = account.viewTracker.peerView(peerId) @@ -348,13 +348,13 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View var rightNavigationButton: ItemListNavigationButton? if let blacklist = blacklist, !blacklist.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 9b1940d888..2260f8c83a 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -659,15 +659,16 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr controller?.dismissAnimated() } let notificationAction: (Int32) -> Void = { muteUntil in - let muteState: PeerMuteState + let muteInterval: Int32? if muteUntil <= 0 { - muteState = .unmuted + muteInterval = nil } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) + muteInterval = Int32.max } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + muteInterval = muteUntil } - changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + + changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start()) } var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, action: { @@ -770,7 +771,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr var leftNavigationButton: ItemListNavigationButton? var rightNavigationButton: ItemListNavigationButton? if let editingState = state.editingState { - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditingState(nil) } @@ -787,9 +788,9 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } if state.savingData { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: view) @@ -830,7 +831,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }) } } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_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 { diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index 3c5debfcda..b8774ee609 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -292,7 +292,8 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo |> mapToSignal { peer in let result = ValuePromise() if let contactsController = contactsController { - let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle)?", actions: [ + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Add \(peer.displayTitle)?", actions: [ TextAlertAction(type: .genericAction, title: "Cancel", action: { result.set(false) }), @@ -411,13 +412,13 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index fa85c42b14..307368715c 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -5,13 +5,21 @@ import SwiftSignalKit import Postbox import TelegramCore +enum ChannelMembersSearchMode { + case searchMembers + case inviteActions +} + private enum ChannelMembersSearchSection { + case none case members case contacts case global - var chatListHeaderType: ChatListSearchItemHeaderType { + var chatListHeaderType: ChatListSearchItemHeaderType? { switch self { + case .none: + return nil case .members: return .members case .contacts: @@ -47,7 +55,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { let peer = self.peer - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: self.peer, chatPeer: self.peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in peerSelected(peer) }) } @@ -56,22 +64,25 @@ struct ChannelMembersSearchContainerTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let isSearching: Bool } -private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ChannelMembersSearchContainerTransition { +private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ChannelMembersSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, peerSelected: peerSelected), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, peerSelected: peerSelected), directionHint: nil) } - return ChannelMembersSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates) + return ChannelMembersSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openPeer: (Peer) -> Void + private let mode: ChannelMembersSearchMode + private let dimNode: ASDisplayNode private let listNode: ListView private var enqueuedTransitions: [(ChannelMembersSearchContainerTransition, Bool)] = [] @@ -85,41 +96,89 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void) { + init(account: Account, peerId: PeerId, mode: ChannelMembersSearchMode, openPeer: @escaping (Peer) -> Void) { self.account = account self.openPeer = openPeer + self.mode = mode self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + self.listNode = ListView() super.init() - self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + self.addSubnode(self.dimNode) self.addSubnode(self.listNode) let themeAndStringsPromise = self.themeAndStringsPromise let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChannelMembersSearchEntry]?, NoError> in if let query = query, !query.isEmpty { - let foundMembers = channelMembers(postbox: account.postbox, network: account.network, peerId: peerId, filter: .search(query)) - let foundContacts = account.postbox.searchContacts(query: query.lowercased()) - let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then(searchPeers(account: account, query: query) - |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + let foundGroupMembers: Signal<[Peer], NoError> + let foundMembers: Signal<[RenderedChannelParticipant], NoError> - return combineLatest(foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get()) - |> map { foundMembers, foundContacts, foundRemotePeers, themeAndStrings -> [ChannelMembersSearchEntry]? in + switch mode { + case .searchMembers: + foundGroupMembers = searchGroupMembers(postbox: account.postbox, network: account.network, peerId: peerId, query: query) + foundMembers = .single([]) + case .inviteActions: + foundGroupMembers = .single([]) + foundMembers = channelMembers(postbox: account.postbox, network: account.network, peerId: peerId, filter: .search(query)) + } + + let foundContacts: Signal<[Peer], NoError> + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> + switch mode { + case .inviteActions: + foundContacts = account.postbox.searchContacts(query: query.lowercased()) + foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + case .searchMembers: + foundContacts = .single([]) + foundRemotePeers = .single(([], [])) + } + + return combineLatest(foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get()) + |> map { foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStrings -> [ChannelMembersSearchEntry]? in var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() var index = 0 + + for peer in foundGroupMembers { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + let section: ChannelMembersSearchSection + switch mode { + case .inviteActions: + section = .members + case .searchMembers: + section = .none + } + entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: section)) + index += 1 + } + } + for participant in foundMembers { if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) - entries.append(ChannelMembersSearchEntry(index: index, peer: participant.peer, section: .members)) + let section: ChannelMembersSearchSection + switch mode { + case .inviteActions: + section = .members + case .searchMembers: + section = .none + } + entries.append(ChannelMembersSearchEntry(index: index, peer: participant.peer, section: section)) index += 1 } } @@ -165,7 +224,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], account: account, theme: themeAndStrings.0, strings: themeAndStrings.1, peerSelected: openPeer) + let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: account, theme: themeAndStrings.0, strings: themeAndStrings.1, peerSelected: openPeer) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -194,8 +253,14 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod self.presentationDataDisposable?.dispose() } + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.backgroundColor = theme.chatList.backgroundColor + self.listNode.backgroundColor = theme.chatList.backgroundColor } override func searchTextUpdated(text: String) { @@ -227,7 +292,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod //options.insert(.AnimateAlpha) } + let isSearching = transition.isSearching self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + self?.dimNode.isHidden = isSearching }) } } @@ -235,6 +303,9 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -267,4 +338,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } } diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index 65f4fabaaf..926903cede 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -28,6 +28,8 @@ final class ChannelMembersSearchController: ViewController { super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.title = self.presentationData.strings.Channel_Members_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } @@ -89,8 +91,8 @@ final class ChannelMembersSearchController: ViewController { private func deactivateSearch(animated: Bool) { if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) self.controllerNode.deactivateSearch(animated: animated) - self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) } } diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index b394ebc424..02422deb54 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -52,7 +52,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { })) for participant in participants { - items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in + items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in if let strongSelf = self { strongSelf.requestOpenPeerFromSearch?(peer) } @@ -135,7 +135,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChannelMembersSearchContainerNode(account: self.account, peerId: self.peerId, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChannelMembersSearchContainerNode(account: self.account, peerId: self.peerId, mode: .inviteActions, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 6182c61916..3851c65256 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -229,11 +229,11 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case let .typeHeader(theme, title): return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) case let .typePublic(theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.publicChannel) }) case let .typePrivate(theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.privateChannel) }) case let .typeInfo(theme, text): @@ -792,7 +792,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } } - rightNavigationButton = ItemListNavigationButton(title: mode == .initialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done, style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(mode == .initialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(state: state, peer: peer) @@ -845,7 +845,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: case .initialSetup: leftNavigationButton = nil case .generic, .privateLink: - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index cb3883c3ca..f1dc691e43 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -7,6 +7,7 @@ import TelegramCore private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0) private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0) +private let messageItalicFont: UIFont = UIFont.italicSystemFont(ofSize: 17.0) private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0) final class ChatBotInfoItem: ListViewItem { @@ -119,7 +120,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all)) } - let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.theme.chat.bubble.infoPrimaryTextColor, linkColor: item.theme.chat.bubble.infoLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) + let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.theme.chat.bubble.infoPrimaryTextColor, linkColor: item.theme.chat.bubble.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, fixedFont: messageFixedFont) let horizontalEdgeInset: CGFloat = 10.0 + params.leftInset let horizontalContentInset: CGFloat = 12.0 @@ -208,7 +209,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { case let .peerMention(peerId, _): foundTapAction = true if let controllerInteraction = self.controllerInteraction { - controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) + controllerInteraction.openPeer(peerId, .chat(textInputState: nil, messageId: nil), nil) } case let .textMention(name): foundTapAction = true diff --git a/TelegramUI/ChatBubbleVideoDecoration.swift b/TelegramUI/ChatBubbleVideoDecoration.swift index 78fc60ceec..15c695ad9c 100644 --- a/TelegramUI/ChatBubbleVideoDecoration.swift +++ b/TelegramUI/ChatBubbleVideoDecoration.swift @@ -4,6 +4,8 @@ import Display import SwiftSignalKit final class ChatBubbleVideoDecoration: UniversalVideoDecoration { + private let nativeSize: CGSize + let backgroundNode: ASDisplayNode? = nil let contentContainerNode: ASDisplayNode let foregroundNode: ASDisplayNode? = nil @@ -12,8 +14,11 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { private var validLayoutSize: CGSize? - init(cornerRadius: CGFloat) { + init(cornerRadius: CGFloat, nativeSize: CGSize) { + self.nativeSize = nativeSize + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.backgroundColor = .black self.contentContainerNode.clipsToBounds = true self.contentContainerNode.cornerRadius = cornerRadius } @@ -32,9 +37,17 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { if let contentNode = contentNode { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) - if let validLayoutSize = self.validLayoutSize { - contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) - contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + if let size = self.validLayoutSize { + var scaledSize = self.nativeSize.aspectFitted(size) + if abs(scaledSize.width - size.width) < 2.0 { + scaledSize.width = size.width + } + if abs(scaledSize.height - size.height) < 2.0 { + scaledSize.height = size.height + } + + contentNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize) + contentNode.updateLayout(size: scaledSize, transition: .immediate) } } } @@ -55,8 +68,15 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { } transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) if let contentNode = self.contentNode { - transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) - contentNode.updateLayout(size: size, transition: transition) + var scaledSize = self.nativeSize.aspectFitted(size) + if abs(scaledSize.width - size.width) < 2.0 { + scaledSize.width = size.width + } + if abs(scaledSize.height - size.height) < 2.0 { + scaledSize.height = size.height + } + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) + contentNode.updateLayout(size: scaledSize, transition: transition) } } diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 38ab2c15da..467275a721 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -193,7 +193,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { peerId = message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil) + controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil) } } case .payment: diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index ed2f3e259d..554ac499a2 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -35,6 +35,20 @@ private enum ChatRecordingActivity { case none } +public enum NavigateToMessageLocation { + case id(MessageId) + case index(MessageIndex) + + var messageId: MessageId { + switch self { + case let .id(id): + return id + case let .index(index): + return index.id + } + } +} + public final class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() @@ -182,9 +196,9 @@ public final class ChatController: TelegramController { } } - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in - if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - return openChatMessage(account: account, message: message, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { + return openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in self?.present(c, in: .window(.root), with: a) @@ -207,7 +221,7 @@ public final class ChatController: TelegramController { }, openUrl: { url in self?.openUrl(url) }, openPeer: { peer, navigation in - self?.openPeer(peerId: peer.id, navigation: navigation, fromMessageId: nil) + self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil) }, callPeer: { peerId in self?.controllerInteraction?.callPeer(peerId) }, sendSticker: { file in @@ -259,20 +273,20 @@ public final class ChatController: TelegramController { strongSelf.secretMediaPreviewController?.dismiss() strongSelf.secretMediaPreviewController = nil } - }, openPeer: { [weak self] id, navigation, fromMessageId in + }, openPeer: { [weak self] id, navigation, fromMessage in if let strongSelf = self { - strongSelf.openPeer(peerId: id, navigation: navigation, fromMessageId: fromMessageId) + strongSelf.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) } }, openPeerMention: { [weak self] name in if let strongSelf = self { strongSelf.openPeerMention(name) } - }, openMessageContextMenu: { [weak self] id, node, frame in + }, openMessageContextMenu: { [weak self] message, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { - if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) { + if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { var updatedMessages = messages for i in 0 ..< updatedMessages.count { - if updatedMessages[i].id == id { + if updatedMessages[i].id == message.id { let message = updatedMessages.remove(at: i) updatedMessages.insert(message, at: 0) break @@ -308,7 +322,7 @@ public final class ChatController: TelegramController { } } }, navigateToMessage: { [weak self] fromId, id in - self?.navigateToMessage(from: fromId, to: id) + self?.navigateToMessage(from: fromId, to: .id(id)) }, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in @@ -448,7 +462,7 @@ public final class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -459,8 +473,8 @@ public final class ChatController: TelegramController { } strongSelf.sendMessages([.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) } - }, openInstantPage: { [weak self] messageId in - if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + }, openInstantPage: { [weak self] message in + if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) } }, openHashtag: { [weak self] peerName, hashtag in @@ -496,7 +510,7 @@ public final class ChatController: TelegramController { return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { peer, current in if let strongSelf = self, let peer = peer, let current = current { - strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) })]), in: .window(.root)) } @@ -559,7 +573,7 @@ public final class ChatController: TelegramController { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) + strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil, messageId: nil), fromMessage: nil) } })) if !mention.isEmpty { @@ -664,11 +678,15 @@ public final class ChatController: TelegramController { return canReplyInChat(strongSelf.presentationInterfaceState) } return false + }, requestMessageUpdate: { [weak self] id in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) + } }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction - self.chatTitleView = ChatTitleView(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatTitleView = ChatTitleView(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { @@ -1362,7 +1380,7 @@ public final class ChatController: TelegramController { }, selectRecentlyUsedInlineBot: { [weak self] peer in if let strongSelf = self, let addressName = peer.addressName { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: "@" + addressName + " ")) }).updatedInputMode({ _ in + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in return .text }) }) @@ -1429,7 +1447,7 @@ public final class ChatController: TelegramController { self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { if let messageId = strongSelf.historyNavigationStack.removeLast() { - strongSelf.navigateToMessage(from: nil, to: messageId.id, rememberInStack: false) + strongSelf.navigateToMessage(from: nil, to: .id(messageId.id), rememberInStack: false) } else { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } @@ -1444,7 +1462,7 @@ public final class ChatController: TelegramController { switch result { case let .result(messageId): if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: messageId) + strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } case .loading: break @@ -1466,7 +1484,14 @@ public final class ChatController: TelegramController { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var updated = state.updatedInterfaceState { - return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: message.text), disableUrlPreview: nil)) + var entities: [MessageTextEntity] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute.entities + break + } + } + return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(message.text, entities: entities)), disableUrlPreview: nil)) } updated = updated.updatedInputMode({ _ in return .text @@ -1489,50 +1514,13 @@ public final class ChatController: TelegramController { }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - strongSelf.messageContextDisposable.set((chatDeleteMessagesOptions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { options in - if let strongSelf = self, !options.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - var items: [ActionSheetItem] = [] - var personalPeerName: String? - var isChannel = false - if let user = strongSelf.presentationInterfaceState.peer as? TelegramUser { - personalPeerName = user.compactDisplayTitle - } else if let channel = strongSelf.presentationInterfaceState.peer as? TelegramChannel, case .broadcast = channel.info { - isChannel = true + strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { actions in + if let strongSelf = self, !actions.options.isEmpty { + if let banAuthor = actions.banAuthor { + strongSelf.presentBanMessageOptions(author: banAuthor, messageIds: messageIds, options: actions.options) + } else { + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) } - - if options.contains(.globally) { - let globalTitle: String - if isChannel { - globalTitle = strongSelf.presentationData.strings.Common_Delete - } else if let personalPeerName = personalPeerName { - globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 - } else { - globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone - } - items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() - } - })) - } - if options.contains(.locally) { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() - } - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) } })) } @@ -1629,7 +1617,14 @@ public final class ChatController: TelegramController { let editingMessage = strongSelf.editingMessage editingMessage.set(true) - strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: editMessage.inputState.inputText, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ + let text = trimChatInputText(editMessage.inputState.inputText) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + var entitiesAttribute: TextEntitiesMessageAttribute? + if !entities.isEmpty { + entitiesAttribute = TextEntitiesMessageAttribute(entities: entities) + } + + strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: editMessage.messageId, text: text.string, entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ editingMessage.set(false) })).start(completed: { if let strongSelf = self { @@ -1676,10 +1671,10 @@ public final class ChatController: TelegramController { } }, navigateMessageSearch: { [weak self] action in if let strongSelf = self { - var navigateId: MessageId? + var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let resultsState = data.resultsState { - if let currentId = resultsState.currentId, let index = resultsState.messageIds.index(of: currentId) { + if let currentId = resultsState.currentId, let index = resultsState.messageIndices.index(where: { $0.id == currentId }) { var updatedIndex: Int? switch action { case .earlier: @@ -1687,20 +1682,25 @@ public final class ChatController: TelegramController { updatedIndex = index - 1 } case .later: - if index != resultsState.messageIds.count - 1 { + if index != resultsState.messageIndices.count - 1 { updatedIndex = index + 1 } } if let updatedIndex = updatedIndex { - navigateId = resultsState.messageIds[updatedIndex] - return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: resultsState.messageIds, currentId: resultsState.messageIds[updatedIndex]))) + navigateIndex = resultsState.messageIndices[updatedIndex] + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: resultsState.messageIndices, currentId: resultsState.messageIndices[updatedIndex].id))) } } } return current }) - if let navigateId = navigateId { - strongSelf.navigateToMessage(from: nil, to: navigateId) + if let navigateIndex = navigateIndex { + switch strongSelf.chatLocation { + case .peer: + strongSelf.navigateToMessage(from: nil, to: .id(navigateIndex.id)) + case .group: + strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex)) + } } } }, openCalendarSearch: { [weak self] in @@ -1714,7 +1714,7 @@ public final class ChatController: TelegramController { if let strongSelf = self { strongSelf.loadingMessage.set(false) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: messageId) + strongSelf.navigateToMessage(from: nil, to: .id(messageId)) } } })) @@ -1742,7 +1742,7 @@ public final class ChatController: TelegramController { }) } }, navigateToMessage: { [weak self] messageId in - self?.navigateToMessage(from: nil, to: messageId) + self?.navigateToMessage(from: nil, to: .id(messageId)) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) }, togglePeerNotifications: { [weak self] in @@ -1764,7 +1764,7 @@ public final class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -1782,7 +1782,7 @@ public final class ChatController: TelegramController { } }, botSwitchChatWithPayload: { [weak self] peerId, payload in if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { - strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId))), fromMessageId: nil) + strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId))), fromMessage: nil) } }, beginMediaRecording: { [weak self] isVideo in if let strongSelf = self { @@ -1842,7 +1842,7 @@ public final class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -1877,7 +1877,7 @@ public final class ChatController: TelegramController { if case .broadcast = channel.info { pinAction(true) } else { - strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { pinAction(false) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { pinAction(true) @@ -1905,7 +1905,7 @@ public final class ChatController: TelegramController { } if canManagePin { - strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { if let strongSelf = self { let disposable: MetaDisposable if let current = strongSelf.unpinMessageDisposable { @@ -1967,6 +1967,10 @@ public final class ChatController: TelegramController { if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.scrollToNextMessage() } + }, openGrouping: { [weak self] in + if let strongSelf = self, case let .group(groupId) = strongSelf.chatLocation { + (strongSelf.navigationController as? NavigationController)?.pushViewController(FeedGroupingController(account: strongSelf.account, groupId: groupId)) + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) switch self.chatLocation { @@ -2049,8 +2053,14 @@ public final class ChatController: TelegramController { } }) - self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId).start(next: { _ in - serviceSoundManager.playMessageDeliveredSound() + self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId).start(next: { [weak self] _ in + if let strongSelf = self { + let inAppNotificationSettings: InAppNotificationSettings = strongSelf.account.telegramApplicationContext.currentInAppNotificationSettings.with { $0 } + + if inAppNotificationSettings.playSounds { + serviceSoundManager.playMessageDeliveredSound() + } + } })) case let .group(groupId): let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total]) @@ -2206,7 +2216,7 @@ public final class ChatController: TelegramController { if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup { if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let _ = keyboardButtonsMessage.visibleButtonKeyboardMarkup { - if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { + if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in return .inputButtons }) @@ -2305,7 +2315,7 @@ public final class ChatController: TelegramController { } } - if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { + if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText.string, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { self.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? @@ -2335,7 +2345,7 @@ public final class ChatController: TelegramController { } } - if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { + if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText.string, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { self.editingUrlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? @@ -2591,7 +2601,8 @@ public final class ChatController: TelegramController { private func sendMessages(_ messages: [EnqueueMessage]) { if case let .peer(peerId) = self.chatLocation { - let _ = enqueueMessages(account: self.account, peerId: peerId, messages: messages).start(next: { [weak self] _ in + let _ = (enqueueMessages(account: self.account, peerId: peerId, messages: messages) + |> deliverOnMainQueue).start(next: { [weak self] _ in self?.chatDisplayNode.historyNode.scrollToEndOfHistory() }) } @@ -2621,7 +2632,7 @@ public final class ChatController: TelegramController { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -2647,7 +2658,7 @@ public final class ChatController: TelegramController { private func activateRaiseGesture() { if let messageToListen = self.firstLoadedMessageToListen() { - let _ = self.controllerInteraction?.openMessage(messageToListen.id) + let _ = self.controllerInteraction?.openMessage(messageToListen) } else { self.requestAudioRecorder(beginWithTone: true) } @@ -2828,10 +2839,6 @@ public final class ChatController: TelegramController { } private func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? { - guard case let .peer(peerId) = self.chatLocation else { - return nil - } - var queryAndLocation: (String, SearchMessagesLocation)? if let search = interfaceState.search { switch search.domain { @@ -2863,6 +2870,8 @@ public final class ChatController: TelegramController { if fromId == nil { queryIsEmpty = true } + } else { + queryIsEmpty = true } } @@ -2883,27 +2892,32 @@ public final class ChatController: TelegramController { } searchDisposable.set((searchMessages(account: self.account, location: location, query: query) |> deliverOnMainQueue).start(next: { [weak self] results in if let strongSelf = self { - var navigateId: MessageId? + var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { - let messageIds = results.map({ $0.id }).sorted() - var currentId = messageIds.last + let messageIndices = results.map({ MessageIndex($0) }).sorted() + var currentIndex = messageIndices.last if let previousResultId = data.resultsState?.currentId { - for id in messageIds { - if id >= previousResultId { - currentId = id + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index break } } } - navigateId = currentId - return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: messageIds, currentId: currentId))) + navigateIndex = currentIndex + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id))) } else { return current } }) - if let navigateId = navigateId { - strongSelf.navigateToMessage(from: nil, to: navigateId) + if let navigateIndex = navigateIndex { + switch strongSelf.chatLocation { + case .peer: + strongSelf.navigateToMessage(from: nil, to: .id(navigateIndex.id)) + case .group: + strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex)) + } } } }, completed: { [weak self] in @@ -2924,45 +2938,52 @@ public final class ChatController: TelegramController { return nil } - public func navigateToMessage(id: MessageId, animated: Bool, completion: (() -> Void)? = nil) { - self.navigateToMessage(from: nil, to: id, rememberInStack: false, animated: animated, completion: completion) + public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, completion: (() -> Void)? = nil) { + self.navigateToMessage(from: nil, to: messageLocation, rememberInStack: false, animated: animated, completion: completion) } - private func navigateToMessage(from fromId: MessageId?, to toId: MessageId, rememberInStack: Bool = true, animated: Bool = true, completion: (() -> Void)? = nil) { + private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, rememberInStack: Bool = true, animated: Bool = true, completion: (() -> Void)? = nil) { if self.isNodeLoaded { - if case let .peer(peerId) = self.chatLocation, toId.peerId == peerId { - var fromIndex: MessageIndex? - - if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { + 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) - } else { - if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { - fromIndex = MessageIndex(message) - } } - + } + + if case let .peer(peerId) = self.chatLocation, messageLocation.messageId.peerId == peerId { if let fromIndex = fromIndex { if let _ = fromId, rememberInStack { self.historyNavigationStack.add(fromIndex) } - if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(toId) { + if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageLocation.messageId) { self.loadingMessage.set(false) self.messageIndexDisposable.set(nil) self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message), animated: animated) completion?() } else { self.loadingMessage.set(true) - let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id): + searchLocation = .id(id) + case let .index(index): + searchLocation = .index(index) + } + let historyView = chatHistoryViewForLocation(.InitialSearch(location: searchLocation, count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in switch historyView { case .Loading: return .complete() - case let .HistoryView(view, _, _, _): + case let .HistoryView(view, _, _, _, _): for entry in view.entries { if case let .MessageEntry(message, _, _, _) = entry { - if message.id == toId { + if message.id == messageLocation.messageId { return .single(MessageIndex(message)) } } @@ -2986,20 +3007,65 @@ public final class ChatController: TelegramController { completion?() } } else { - completion?() - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(toId.peerId), messageId: toId)) + if let fromIndex = fromIndex { + if let _ = fromId, rememberInStack { + self.historyNavigationStack.add(fromIndex) + } + + self.loadingMessage.set(true) + let searchLocation: ChatHistoryInitialSearchLocation + switch messageLocation { + case let .id(id): + searchLocation = .id(id) + case let .index(index): + searchLocation = .index(index) + } + let historyView = chatHistoryViewForLocation(.InitialSearch(location: searchLocation, count: 50), account: self.account, chatLocation: self.chatLocation, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) + let signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .complete() + case let .HistoryView(view, _, _, _, _): + for entry in view.entries { + if case let .MessageEntry(message, _, _, _) = entry { + if message.id == messageLocation.messageId { + return .single(MessageIndex(message)) + } + } + } + return .single(nil) + } + } + |> take(1) + self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in + if let strongSelf = self { + if let index = index { + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated) + completion?() + } else { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(messageLocation.messageId.peerId), messageId: messageLocation.messageId)) + completion?() + } + } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(false) + } + })) + } } } else { completion?() } } - private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessageId: MessageId?) { + private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { case .info: self.navigationButtonAction(.openChatInfo) - case let .chat(textInputState): + case let .chat(textInputState, _): if let textInputState = textInputState { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { return ($0.updatedInterfaceState { @@ -3021,8 +3087,8 @@ public final class ChatController: TelegramController { switch navigation { case .info: let peerSignal: Signal - if let fromMessageId = fromMessageId { - peerSignal = loadedPeerFromMessage(account: self.account, peerId: peerId, messageId: fromMessageId) + if let fromMessage = fromMessage { + peerSignal = loadedPeerFromMessage(account: self.account, peerId: peerId, messageId: fromMessage.id) } else { peerSignal = self.account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) } } @@ -3033,7 +3099,7 @@ public final class ChatController: TelegramController { } } })) - case let .chat(textInputState): + case let .chat(textInputState, messageId): if let textInputState = textInputState { let _ = (self.account.postbox.modify({ modifier -> Void in modifier.updatePeerChatInterfaceState(peerId, update: { currentState in @@ -3055,13 +3121,13 @@ public final class ChatController: TelegramController { (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: nil, botStart: botStart)) } case .group: - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: fromMessageId, botStart: nil)) + (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, chatLocation: .peer(peerId), messageId: fromMessage?.id, botStart: nil)) } } else { switch navigation { case .info: break - case let .chat(textInputState): + case let .chat(textInputState, _): if let textInputState = textInputState { let controller = PeerSelectionController(account: self.account) controller.peerSelected = { [weak self, weak controller] peerId in @@ -3217,32 +3283,26 @@ public final class ChatController: TelegramController { } disposable.set((resolveUrl(account: self.account, url: url) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { - switch result { - case let .externalUrl(url): - openExternalUrl(url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.navigationController as? NavigationController) - case let .peer(peerId): - strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) - case let .botStart(peerId, payload): - strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)), fromMessageId: nil) - case let .groupBotStart(peerId, payload): - break - case let .channelMessage(peerId, messageId): - if case .peer(peerId) = strongSelf.chatLocation { - strongSelf.navigateToMessage(from: nil, to: messageId) - } else { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId)) + openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.navigationController as? NavigationController, openPeer: { peerId, navigation in + if let strongSelf = self { + switch navigation { + case let .chat(_, messageId): + if case .peer(peerId) = strongSelf.chatLocation { + if let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: .id(messageId)) + } + } else { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId)) + } + case .info: + break + case let .withBotStartPayload(startPayload): + break } - case let .stickerPack(name): - strongSelf.present(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name)), in: .window(.root)) - case let .instantView(webpage, anchor): - (strongSelf.navigationController as? NavigationController)?.pushViewController(InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor)) - case let .join(link): - strongSelf.present(JoinLinkPreviewController(account: strongSelf.account, link: link, navigateToPeer: { peerId in - if let strongSelf = self { - strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) - } - }), in: .window(.root)) - } + } + }, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }) } })) } @@ -3306,7 +3366,7 @@ public final class ChatController: TelegramController { items.append(UIPreviewAction(title: title, style: .default, handler: { _, _ in if let strongSelf = self { - let _ = mutePeer(account: strongSelf.account, peerId: peer.id, for: muteInterval).start() + let _ = updatePeerMuteSetting(account: strongSelf.account, peerId: peer.id, muteInterval: muteInterval).start() } })) } @@ -3323,7 +3383,7 @@ public final class ChatController: TelegramController { } private func debugStreamSingleVideo(_ id: MessageId) { - let gallery = GalleryController(account: self.account, messageId: id, streamSingleVideo: true, replaceRootController: { [weak self] controller, ready in + let gallery = GalleryController(account: self.account, source: .peerMessagesAtId(id), streamSingleVideo: true, replaceRootController: { [weak self] controller, ready in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) } @@ -3351,4 +3411,136 @@ public final class ChatController: TelegramController { return nil })) } + + private func presentBanMessageOptions(author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { + if case let .peer(peerId) = self.chatLocation { + self.navigationActionDisposable.set((fetchChannelParticipant(account: self.account, peerId: peerId, participantId: author.id) + |> deliverOnMainQueue).start(next: { [weak self] participant in + if let strongSelf = self { + var canBan = true + if let participant = participant { + switch participant { + case .creator: + canBan = false + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo, !adminInfo.rights.flags.isEmpty { + canBan = false + } + } + } + if canBan { + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + var items: [ActionSheetItem] = [] + + var actions = Set([0]) + + let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in + if actions.contains(category) { + actions.remove(category) + } else { + actions.insert(category) + } + actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + } + + var itemIndex = 0 + for categoryId in [0, 1, 2, 3] as [Int] { + var title = "" + if categoryId == 0 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Delete + } else if categoryId == 1 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Ban + } else if categoryId == 2 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Report + } else if categoryId == 3 { + title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(author.displayTitle).0 + } + let index = itemIndex + items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in + toggleCheck(categoryId, index) + })) + itemIndex += 1 + } + + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if actions.contains(3) { + let _ = strongSelf.account.postbox.modify({ modifier -> Void in + modifier.removeAllMessagesWithAuthor(peerId, authorId: author.id) + }).start() + let _ = clearAuthorHistory(account: strongSelf.account, peerId: peerId, memberId: author.id).start() + } else if actions.contains(0) { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + } + if actions.contains(1) { + let _ = removePeerMember(account: strongSelf.account, peerId: peerId, memberId: author.id).start() + } + } + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window(.root)) + } else { + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: options) + } + } + })) + } + } + + private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions) { + let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = self.presentationInterfaceState.peer as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = self.presentationInterfaceState.peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if options.contains(.deleteGlobally) { + let globalTitle: String + if isChannel { + globalTitle = self.presentationData.strings.Common_Delete + } else if let personalPeerName = personalPeerName { + globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 + } else { + globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone + } + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + } + })) + } + if options.contains(.deleteLocally) { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.present(actionSheet, in: .window(.root)) + } } diff --git a/TelegramUI/ChatControllerBackground.swift b/TelegramUI/ChatControllerBackground.swift new file mode 100644 index 0000000000..4af35e6791 --- /dev/null +++ b/TelegramUI/ChatControllerBackground.swift @@ -0,0 +1,35 @@ +import Foundation +import TelegramCore +import Display +import Postbox + +private var backgroundImageForWallpaper: (TelegramWallpaper, UIImage)? + +func chatControllerBackgroundImage(wallpaper: TelegramWallpaper, postbox: Postbox) -> UIImage? { + var backgroundImage: UIImage? + if wallpaper == backgroundImageForWallpaper?.0 { + backgroundImage = backgroundImageForWallpaper?.1 + } else { + switch wallpaper { + case .builtin: + if let filePath = frameworkBundle.path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { + backgroundImage = UIImage(contentsOfFile: filePath)?.precomposed() + } + case let .color(color): + backgroundImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) + case let .image(representations): + if let largest = largestImageRepresentation(representations) { + if let path = postbox.mediaBox.completedResourcePath(largest.resource) { + backgroundImage = UIImage(contentsOfFile: path)?.precomposed() + } + } + } + if let backgroundImage = backgroundImage { + backgroundImageForWallpaper = (wallpaper, backgroundImage) + } + } + return backgroundImage +} diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 40fa587fff..73a0bfe987 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -15,7 +15,7 @@ public struct ChatControllerInitialBotStart { } public enum ChatControllerInteractionNavigateToPeer { - case chat(textInputState: ChatTextInputState?) + case chat(textInputState: ChatTextInputState?, messageId: MessageId?) case info case withBotStartPayload(ChatControllerInitialBotStart) } @@ -37,12 +37,12 @@ public enum ChatControllerInteractionLongTapAction { } public final class ChatControllerInteraction { - let openMessage: (MessageId) -> Bool + let openMessage: (Message) -> Bool let openSecretMessagePreview: (MessageId) -> Void let closeSecretMessagePreview: () -> Void - let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void + let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void - let openMessageContextMenu: (MessageId, ASDisplayNode, CGRect) -> Void + let openMessageContextMenu: (Message, ASDisplayNode, CGRect) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let clickThroughMessage: () -> Void let toggleMessagesSelection: ([MessageId], Bool) -> Void @@ -54,7 +54,7 @@ public final class ChatControllerInteraction { let shareCurrentLocation: () -> Void let shareAccountContact: () -> Void let sendBotCommand: (MessageId?, String) -> Void - let openInstantPage: (MessageId) -> Void + let openInstantPage: (Message) -> Void let openHashtag: (String?, String) -> Void let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void let openMessageShareMenu: (MessageId) -> Void @@ -66,12 +66,14 @@ public final class ChatControllerInteraction { let setupReply: (MessageId) -> Void let canSetupReply: () -> Bool + let requestMessageUpdate: (MessageId) -> Void + var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? var highlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (MessageId) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -101,6 +103,8 @@ public final class ChatControllerInteraction { self.setupReply = setupReply self.canSetupReply = canSetupReply + self.requestMessageUpdate = requestMessageUpdate + self.automaticMediaDownloadSettings = automaticMediaDownloadSettings } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index a7a75f5da7..a28b414f3d 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -5,13 +5,6 @@ import SwiftSignalKit import Display import TelegramCore -private var backgroundImageForWallpaper: (TelegramWallpaper, UIImage)? - -private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: ChatPresentationInterfaceState, _ rhs: ChatPresentationInterfaceState) -> Bool { - - return false -} - private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider { var inputAccessoryHeight: (() -> CGFloat)? @@ -166,34 +159,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - var backgroundImage: UIImage? - let wallpaper = chatPresentationInterfaceState.chatWallpaper - if wallpaper == backgroundImageForWallpaper?.0 { - backgroundImage = backgroundImageForWallpaper?.1 - } else { - switch wallpaper { - case .builtin: - if let filePath = frameworkBundle.path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { - backgroundImage = UIImage(contentsOfFile: filePath)?.precomposed() - } - case let .color(color): - backgroundImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in - context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - }) - case let .image(representations): - if let largest = largestImageRepresentation(representations) { - if let path = account.postbox.mediaBox.completedResourcePath(largest.resource) { - backgroundImage = UIImage(contentsOfFile: path)?.precomposed() - } - } - } - if let backgroundImage = backgroundImage { - backgroundImageForWallpaper = (wallpaper, backgroundImage) - } - } - - self.backgroundNode.contents = backgroundImage?.cgImage + self.backgroundNode.contents = chatControllerBackgroundImage(wallpaper: chatPresentationInterfaceState.chatWallpaper, postbox: account.postbox)?.cgImage self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNode) @@ -225,29 +191,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } - if let editMessage = effectivePresentationInterfaceState.interfaceState.editMessage { - let text = editMessage.inputState.inputText - - if let interfaceInteraction = strongSelf.interfaceInteraction { - interfaceInteraction.editMessage() - } + if let _ = effectivePresentationInterfaceState.interfaceState.editMessage { + strongSelf.interfaceInteraction?.editMessage() } else { - let text = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText + var messages: [EnqueueMessage] = [] - if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { - strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in - if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { - strongSelf.ignoreUpdateHeight = true - textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) - strongSelf.ignoreUpdateHeight = false - } - }) - - var messages: [EnqueueMessage] = [] - if !text.isEmpty { + for text in breakChatInputText(trimChatInputText(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)) { + if text.length != 0 { var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text, enabledTypes: .all) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } @@ -257,8 +209,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { webpage = strongSelf.chatPresentationInterfaceState.urlPreview?.1 } - messages.append(.message(text: text, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) + messages.append(.message(text: text.string, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) } + } + + if !messages.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { + strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in + if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { + strongSelf.ignoreUpdateHeight = true + textInputPanelNode.text = "" + strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) + strongSelf.ignoreUpdateHeight = false + } + }) + + if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { for id in forwardMessageIds { messages.append(.forward(source: id, grouping: .auto)) @@ -266,7 +231,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if case let .peer(peerId) = strongSelf.chatLocation { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start(next: { _ in + let _ = (enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages) |> deliverOnMainQueue).start(next: { _ in if let strongSelf = self { strongSelf.historyNode.scrollToEndOfHistory() } @@ -412,7 +377,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var activate = false if self.searchNavigationNode == nil { activate = true - self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, interaction: interfaceInteraction) + self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, chatLocation: self.chatPresentationInterfaceState.chatLocation, interaction: interfaceInteraction) } self.navigationBar.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated) self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState) diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index c84ab4483e..1dcb087005 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -226,7 +226,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } return .complete() - case let .HistoryView(view, type, scrollPosition, _): + case let .HistoryView(view, type, scrollPosition, _, _): let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false switch type { diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 061a85ab61..590b11e578 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -54,7 +54,7 @@ public struct ChatHistoryCombinedInitialData { enum ChatHistoryViewUpdate { case Loading(initialData: ChatHistoryCombinedInitialData?) - case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) + case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, originalScrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) } struct ChatHistoryView { @@ -368,7 +368,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { |> mapToSignal { location in return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in switch viewUpdate { - case let .HistoryView(view, _, _, _): + case let .HistoryView(view, _, _, _, _): let _ = fixedCombinedReadStates.swap(view.combinedReadStates) default: break @@ -406,7 +406,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } return .complete() - case let .HistoryView(view, type, scrollPosition, data): + case let .HistoryView(view, type, scrollPosition, originalScrollPosition, data): initialData = data var updatedScrollPosition = scrollPosition @@ -421,6 +421,19 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData)) let previous = previousView.swap(processedView) + if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { + switch originalScrollPosition { + case let .index(index, position, _, _): + if case .upperBound = index { + if let previous = previous, previous.filteredEntries.count == 1, case .HoleEntry = previous.filteredEntries[0] { + updatedScrollPosition = .index(index: index, position: position, directionHint: .Down, animated: false) + } + } + default: + break + } + } + let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false @@ -477,7 +490,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if apply { switch chatLocation { case .peer: - let _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start() + let _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, stateManager: account.stateManager, index: messageIndex).start() case let .group(groupId): let _ = account.postbox.modify({ modifier -> Void in modifier.applyGroupFeedInteractiveReadMaxIndex(groupId: groupId, index: messageIndex) @@ -673,7 +686,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool) { + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true) { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: .center(.bottom), animated: animated)) } @@ -985,14 +998,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !nextItem { scrolled = true - self.scrollToMessage(from: scrollState.messageIndex, to: scrollState.messageIndex, animated: true) + self.scrollToMessage(from: scrollState.messageIndex, to: scrollState.messageIndex, animated: true, highlight: false) } else { loop: for i in (index + 1) ..< historyView.filteredEntries.count { let entry = historyView.filteredEntries[i] switch entry { case .MessageEntry, .MessageGroupEntry: scrolled = true - self.scrollToMessage(from: scrollState.messageIndex, to: entry.index, animated: true) + self.scrollToMessage(from: scrollState.messageIndex, to: entry.index, animated: true, highlight: false) break loop default: break @@ -1007,4 +1020,29 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + func requestMessageUpdate(_ id: MessageId) { + if let historyView = self.historyView { + loop: for i in 0 ..< historyView.filteredEntries.count { + switch historyView.filteredEntries[i] { + case let .MessageEntry(message, presentationData, read, _, selection): + if message.id == id { + let index = historyView.filteredEntries.count - 1 - i + let item: ListViewItem + switch self.mode { + case .bubbles: + item = ChatMessageItem(presentationData: presentationData, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection)) + case let .list(search, _): + item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: search) + } + let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + break loop + } + default: + break + } + } + } + } } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 627ba4d2ca..00d74293fa 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -21,7 +21,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, originalScrollPosition: nil, initialData: combinedInitialData) } else { if view.isLoading { return .Loading(initialData: combinedInitialData) @@ -81,7 +81,7 @@ 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, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } case let .InitialSearch(searchLocation, count): @@ -102,7 +102,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, originalScrollPosition: nil, initialData: combinedInitialData) } else { let anchorIndex = view.anchorIndex @@ -126,7 +126,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - 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, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } case let .Navigation(index, anchorIndex, count): @@ -141,12 +141,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - /*print("————————") - print("entries for navigation around \(index)") - print("--------") - print("\(view.entries.map { $0.index.id })") - print("————————")*/ - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up @@ -163,7 +158,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, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, originalScrollPosition: chatScrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } } diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index fd3fc86352..1a130b1fbb 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -11,6 +11,8 @@ private enum ChatInfoTitleButton { case unmute case call case report + case grouping + case channels func title(_ strings: PresentationStrings) -> String { switch self { @@ -26,6 +28,10 @@ private enum ChatInfoTitleButton { return strings.Conversation_Call case .report: return strings.ReportPeer_Report + case .grouping: + return "Grouping" + case .channels: + return "Channels" } } @@ -33,7 +39,7 @@ private enum ChatInfoTitleButton { switch self { case .search: return PresentationResourcesChat.chatTitlePanelSearchImage(theme) - case .info: + case .info, .channels: return PresentationResourcesChat.chatTitlePanelInfoImage(theme) case .mute: return PresentationResourcesChat.chatTitlePanelMuteImage(theme) @@ -43,6 +49,8 @@ private enum ChatInfoTitleButton { return PresentationResourcesChat.chatTitlePanelCallImage(theme) case .report: return PresentationResourcesChat.chatTitlePanelReportImage(theme) + case .grouping: + return PresentationResourcesChat.chatTitlePanelGroupingImage(theme) } } } @@ -79,6 +87,10 @@ private func peerButtons(_ peer: Peer, isMuted: Bool) -> [ChatInfoTitleButton] { } } +private func groupButtons() -> [ChatInfoTitleButton] { + return [.search, .grouping, .channels] +} + private let buttonFont = Font.regular(10.0) private final class ChatInfoTitlePanelButtonNode: HighlightableButtonNode { @@ -130,10 +142,15 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { } let updatedButtons: [ChatInfoTitleButton] - if let peer = interfaceState.peer { - updatedButtons = peerButtons(peer, isMuted: interfaceState.peerIsMuted) - } else { - updatedButtons = [] + switch interfaceState.chatLocation { + case .peer: + if let peer = interfaceState.peer { + updatedButtons = peerButtons(peer, isMuted: interfaceState.peerIsMuted) + } else { + updatedButtons = [] + } + case .group: + updatedButtons = groupButtons() } var buttonsUpdated = false @@ -183,7 +200,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { for (button, buttonNode) in self.buttons { if buttonNode === node { switch button { - case .info: + case .info, .channels: self.interfaceInteraction?.openPeerInfo() case .mute: self.interfaceInteraction?.togglePeerNotifications() @@ -195,6 +212,9 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { self.interfaceInteraction?.beginCall() case .report: self.interfaceInteraction?.reportPeer() + case .grouping: + self.interfaceInteraction?.openGrouping() + break } break } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 57c767a3b3..0946abea7d 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -23,103 +23,108 @@ struct PossibleContextQueryTypes: OptionSet { private func makeScalar(_ c: Character) -> Character { return c - //return c.utf16[c.utf16.startIndex] } -private let spaceScalar = makeScalar(" ") -private let newlineScalar = makeScalar("\n") -private let hashScalar = makeScalar("#") -private let atScalar = makeScalar("@") -private let slashScalar = makeScalar("/") +private let spaceScalar = " " as UnicodeScalar +private let newlineScalar = "\n" as UnicodeScalar +private let hashScalar = "#" as UnicodeScalar +private let atScalar = "@" as UnicodeScalar +private let slashScalar = "/" as UnicodeScalar private let alphanumerics = CharacterSet.alphanumerics -func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(Range, PossibleContextQueryTypes, Range?)] { +func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] { + if inputState.selectionRange.count != 0 { + return [] + } + let inputText = inputState.inputText - var results: [(Range, PossibleContextQueryTypes, Range?)] = [] - if !inputText.isEmpty { - if inputText.hasPrefix("@") && inputText != "@" { - let startIndex = inputText.index(after: inputText.startIndex) + let inputString: NSString = inputText.string as NSString + var results: [(NSRange, PossibleContextQueryTypes, NSRange?)] = [] + let inputLength = inputString.length + + if inputLength != 0 { + if inputString.hasPrefix("@") && inputLength != 1 { + let startIndex = 1 var index = startIndex - var contextAddressRange: Range? + var contextAddressRange: NSRange? while true { - if index == inputText.endIndex { + if index == inputLength { break } - let c = inputText[index] - - if c == " " { - if index != startIndex { - contextAddressRange = startIndex ..< index - index = inputText.index(after: index) - } - break - } else { - if !((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_") { + if let c = UnicodeScalar(inputString.character(at: index)) { + if c == " " { + if index != startIndex { + contextAddressRange = NSRange(location: startIndex, length: index - startIndex) + index += 1 + } break + } else { + if !((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_") { + break + } + } + + if index == inputLength { + break + } else { + index += 1 } - } - - if index == inputText.endIndex { - break } else { - index = inputText.index(after: index) + index += 1 } } if let contextAddressRange = contextAddressRange { - results.append((contextAddressRange, [.contextRequest], index ..< inputText.endIndex)) + results.append((contextAddressRange, [.contextRequest], NSRange(location: index, length: inputLength - index))) } } - let maxUtfIndex = inputText.utf16.index(inputText.utf16.startIndex, offsetBy: inputState.selectionRange.lowerBound) - guard let maxIndex = maxUtfIndex.samePosition(in: inputText) else { + let maxIndex = min(inputState.selectionRange.lowerBound, inputLength) + if maxIndex == 0 { return results } - if maxIndex == inputText.startIndex { - return results - } - var index = inputText.index(before: maxIndex) + var index = maxIndex - 1 - var possibleQueryRange: Range? + var possibleQueryRange: NSRange? - if inputText.isSingleEmoji { - return [(inputText.startIndex ..< inputText.endIndex, [.emoji], nil)] + if (inputString as String).isSingleEmoji { + return [(NSRange(location: 0, length: inputLength), [.emoji], nil)] } var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag]) var definedType = false while true { - let c = inputText[index] - - if c == spaceScalar || c == newlineScalar { - possibleTypes = [] - } else if c == hashScalar { - possibleTypes = possibleTypes.intersection([.hashtag]) - definedType = true - index = inputText.index(after: index) - possibleQueryRange = index ..< maxIndex - break - } else if c == atScalar { - possibleTypes = possibleTypes.intersection([.mention]) - definedType = true - index = inputText.index(after: index) - possibleQueryRange = index ..< maxIndex - break - } else if c == slashScalar { - possibleTypes = possibleTypes.intersection([.command]) - definedType = true - index = inputText.index(after: index) - possibleQueryRange = index ..< maxIndex - break + if let c = UnicodeScalar(inputString.character(at: index)) { + if c == spaceScalar || c == newlineScalar { + possibleTypes = [] + } else if c == hashScalar { + possibleTypes = possibleTypes.intersection([.hashtag]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + break + } else if c == atScalar { + possibleTypes = possibleTypes.intersection([.mention]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + break + } else if c == slashScalar { + possibleTypes = possibleTypes.intersection([.command]) + definedType = true + index += 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) + break + } } - if index == inputText.startIndex { + if index == 0 { break } else { - index = inputText.index(before: index) - possibleQueryRange = index ..< maxIndex + index -= 1 + possibleQueryRange = NSRange(location: index, length: maxIndex - index) } } @@ -132,23 +137,24 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> [ChatPresentationInputQuery] { let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState + let inputString: NSString = inputState.inputText.string as NSString var result: [ChatPresentationInputQuery] = [] for (possibleQueryRange, possibleTypes, additionalStringRange) in textInputStateContextQueryRangeAndType(inputState) { - let query = String(inputState.inputText[possibleQueryRange]) + let query = inputString.substring(with: possibleQueryRange) if possibleTypes == [.emoji] { result.append(.emoji(query)) } else if possibleTypes == [.hashtag] { result.append(.hashtag(query)) } else if possibleTypes == [.mention] { var types: ChatInputQueryMentionTypes = [.members] - if possibleQueryRange.lowerBound == inputState.inputText.index(after: inputState.inputText.startIndex) { + if possibleQueryRange.lowerBound == 1 { types.insert(.contextBots) } result.append(.mention(query: query, types: types)) } else if possibleTypes == [.command] { result.append(.command(query)) } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { - let additionalString = String(inputState.inputText[additionalStringRange]) + let additionalString = inputString.substring(with: additionalStringRange) result.append(.contextRequest(addressName: query, query: additionalString)) } } @@ -162,9 +168,11 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) for inputQuery in inputQueries { if case let .contextRequest(addressName, query) = inputQuery, query.isEmpty { + let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize) + let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(17.0), textColor: UIColor.clear)) - string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)) + string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(baseFontSize), textColor: UIColor.clear)) + string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(baseFontSize), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)) contextPlaceholder = string } } @@ -174,8 +182,10 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } switch chatPresentationInterfaceState.inputMode { case .media: - if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty && chatPresentationInterfaceState.inputMode == .media(.gif) { - contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(17.0), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor) + if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && chatPresentationInterfaceState.inputMode == .media(.gif) { + let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize) + + contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(baseFontSize), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor) } return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) case .inputButtons: @@ -184,7 +194,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if let _ = chatPresentationInterfaceState.interfaceState.editMessage { return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState) } else { - if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty { + if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 { var accessoryItems: [ChatTextInputAccessoryItem] = [] if let peer = chatPresentationInterfaceState.peer as? TelegramSecretChat { accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index d44e6da127..e12b0c1fe1 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -28,39 +28,196 @@ struct ChatInterfaceSelectionState: PostboxCoding, Equatable { } } +private enum ChatTextInputStateTextAttributeType: PostboxCoding, Equatable { + case bold + case italic + case monospace + case textMention(PeerId) + + init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("t", orElse: 0) { + case 0: + self = .bold + case 1: + self = .italic + case 2: + self = .monospace + case 3: + self = .textMention(PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0))) + default: + assertionFailure() + self = .bold + } + } + + func encode(_ encoder: PostboxEncoder) { + switch self { + case .bold: + encoder.encodeInt32(0, forKey: "t") + case .italic: + encoder.encodeInt32(1, forKey: "t") + case .monospace: + encoder.encodeInt32(2, forKey: "t") + case let .textMention(id): + encoder.encodeInt32(3, forKey: "t") + encoder.encodeInt64(id.toInt64(), forKey: "peerId") + } + } + + static func ==(lhs: ChatTextInputStateTextAttributeType, rhs: ChatTextInputStateTextAttributeType) -> Bool { + switch lhs { + case .bold: + if case .bold = rhs { + return true + } else { + return false + } + case .italic: + if case .italic = rhs { + return true + } else { + return false + } + case .monospace: + if case .monospace = rhs { + return true + } else { + return false + } + case let .textMention(id): + if case .textMention(id) = rhs { + return true + } else { + return false + } + } + } +} + +private struct ChatTextInputStateTextAttribute: PostboxCoding, Equatable { + let type: ChatTextInputStateTextAttributeType + let range: Range + + init(type: ChatTextInputStateTextAttributeType, range: Range) { + self.type = type + self.range = range + } + + init(decoder: PostboxDecoder) { + self.type = decoder.decodeObjectForKey("type", decoder: { ChatTextInputStateTextAttributeType(decoder: $0) }) as! ChatTextInputStateTextAttributeType + self.range = Int(decoder.decodeInt32ForKey("range0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("range1", orElse: 0)) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.type, forKey: "type") + encoder.encodeInt32(Int32(self.range.lowerBound), forKey: "range0") + encoder.encodeInt32(Int32(self.range.upperBound), forKey: "range1") + } + + static func ==(lhs: ChatTextInputStateTextAttribute, rhs: ChatTextInputStateTextAttribute) -> Bool { + return lhs.type == rhs.type && lhs.range == rhs.range + } +} + +private struct ChatTextInputStateText: PostboxCoding, Equatable { + let text: String + fileprivate let attributes: [ChatTextInputStateTextAttribute] + + init() { + self.text = "" + self.attributes = [] + } + + init(text: String, attributes: [ChatTextInputStateTextAttribute]) { + self.text = text + self.attributes = attributes + } + + init(attributedText: NSAttributedString) { + self.text = attributedText.string + var parsedAttributes: [ChatTextInputStateTextAttribute] = [] + attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: [], using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .bold, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.italic { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .italic, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.monospace { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .monospace, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.textMention, let value = value as? ChatTextInputTextMentionAttribute { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textMention(value.peerId), range: range.location ..< (range.location + range.length))) + } + } + }) + self.attributes = parsedAttributes + } + + init(decoder: PostboxDecoder) { + self.text = decoder.decodeStringForKey("text", orElse: "") + self.attributes = decoder.decodeObjectArrayWithDecoderForKey("attributes") + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.text, forKey: "text") + encoder.encodeObjectArray(self.attributes, forKey: "attributes") + } + + static func ==(lhs: ChatTextInputStateText, rhs: ChatTextInputStateText) -> Bool { + return lhs.text == rhs.text && lhs.attributes == rhs.attributes + } + + func attributedText() -> NSAttributedString { + let result = NSMutableAttributedString(string: self.text) + for attribute in self.attributes { + switch attribute.type { + case .bold: + result.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .italic: + result.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .monospace: + result.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case let .textMention(id): + result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + } + } + return result + } +} + public struct ChatTextInputState: PostboxCoding, Equatable { - let inputText: String + let inputText: NSAttributedString let selectionRange: Range public static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool { - return lhs.inputText == rhs.inputText && lhs.selectionRange == rhs.selectionRange + return lhs.inputText.isEqual(to: rhs.inputText) && lhs.selectionRange == rhs.selectionRange } init() { - self.inputText = "" + self.inputText = NSAttributedString() self.selectionRange = 0 ..< 0 } - init(inputText: String, selectionRange: Range) { + init(inputText: NSAttributedString, selectionRange: Range) { self.inputText = inputText self.selectionRange = selectionRange } - init(inputText: String) { + init(inputText: NSAttributedString) { self.inputText = inputText - let length = (inputText as NSString).length + let length = inputText.length self.selectionRange = length ..< length } public init(decoder: PostboxDecoder) { - self.inputText = decoder.decodeStringForKey("t", orElse: "") - self.selectionRange = Int(decoder.decodeInt32ForKey("s0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("s1", orElse: 0)) + self.inputText = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText() + self.selectionRange = Int(decoder.decodeInt32ForKey("as0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("as1", orElse: 0)) } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeString(self.inputText, forKey: "t") - encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0") - encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1") + encoder.encodeObject(ChatTextInputStateText(attributedText: self.inputText), forKey: "at") + + encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "as0") + encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "as1") } } @@ -112,26 +269,26 @@ struct ChatEditMessageState: PostboxCoding, Equatable { final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { let timestamp: Int32 - let text: String + let text: NSAttributedString - init(timestamp: Int32, text: String) { + init(timestamp: Int32, text: NSAttributedString) { self.timestamp = timestamp self.text = text } init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0) - self.text = decoder.decodeStringForKey("t", orElse: "") + self.text = ((decoder.decodeObjectForKey("at", decoder: { ChatTextInputStateText(decoder: $0) }) as? ChatTextInputStateText) ?? ChatTextInputStateText()).attributedText() } func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "d") - encoder.encodeString(self.text, forKey: "t") + encoder.encodeObject(ChatTextInputStateText(attributedText: self.text), forKey: "at") } public func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool { if let to = to as? ChatEmbeddedInterfaceState { - return self.timestamp == to.timestamp && self.text == to.text + return self.timestamp == to.timestamp && self.text.isEqual(to: to.text) } else { return false } @@ -274,7 +431,7 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { - if !self.composeInputState.inputText.isEmpty && self.timestamp != 0 { + if self.composeInputState.inputText.length != 0 && self.timestamp != 0 { return ChatEmbeddedInterfaceState(timestamp: self.timestamp, text: self.composeInputState.inputText) } else { return nil @@ -282,10 +439,10 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } var synchronizeableInputState: SynchronizeableChatInputState? { - if self.composeInputState.inputText.isEmpty { + if self.composeInputState.inputText.length == 0 { return nil } else { - return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.composeInputState.inputText, timestamp: self.timestamp) + return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.composeInputState.inputText.string, timestamp: self.timestamp) } } @@ -294,7 +451,7 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> SynchronizeableChatInterfaceState { - var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId) + var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: state?.text ?? ""))).withUpdatedReplyMessageId(state?.replyToMessageId) if let timestamp = state?.timestamp { result = result.withUpdatedTimestamp(timestamp) } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index ec617e6f32..235d70b1dc 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -75,26 +75,28 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: var canReply = false var canPin = false - switch chatPresentationInterfaceState.chatLocation { - case .peer: - if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel { - switch channel.info { - case .broadcast: - canReply = channel.hasAdminRights([.canPostMessages]) - if !isAction { - canPin = channel.hasAdminRights([.canEditMessages]) - } - case .group: - canReply = true - if !isAction { - canPin = channel.hasAdminRights([.canPinMessages]) - } + if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { + switch chatPresentationInterfaceState.chatLocation { + case .peer: + if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel { + switch channel.info { + case .broadcast: + canReply = channel.hasAdminRights([.canPostMessages]) + if !isAction { + canPin = channel.hasAdminRights([.canEditMessages]) + } + case .group: + canReply = true + if !isAction { + canPin = channel.hasAdminRights([.canPinMessages]) + } + } + } else { + canReply = true } - } else { - canReply = true - } - case .group: - break + case .group: + break + } } var canEdit = false @@ -215,10 +217,11 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = UIImage(data: imageData) { if !message.text.isEmpty { - UIPasteboard.general.items = [ + UIPasteboard.general.string = message.text + /*UIPasteboard.general.items = [ [kUTTypeUTF8PlainText as String: message.text], [kUTTypePNG as String: image] - ] + ]*/ } else { UIPasteboard.general.image = image } @@ -282,7 +285,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } -struct ChatDeleteMessagesOptions: OptionSet { +struct ChatAvailableMessageActionOptions: OptionSet { var rawValue: Int32 init(rawValue: Int32) { @@ -293,57 +296,76 @@ struct ChatDeleteMessagesOptions: OptionSet { self.rawValue = 0 } - static let locally = ChatDeleteMessagesOptions(rawValue: 1 << 0) - static let globally = ChatDeleteMessagesOptions(rawValue: 1 << 1) + static let deleteLocally = ChatAvailableMessageActionOptions(rawValue: 1 << 0) + static let deleteGlobally = ChatAvailableMessageActionOptions(rawValue: 1 << 1) + static let forward = ChatAvailableMessageActionOptions(rawValue: 1 << 2) } -func chatDeleteMessagesOptions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal { - return postbox.modify { modifier -> ChatDeleteMessagesOptions in - var optionsMap: [MessageId: ChatDeleteMessagesOptions] = [:] +struct ChatAvailableMessageActions { + let options: ChatAvailableMessageActionOptions + let banAuthor: Peer? +} + +func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal { + return postbox.modify { modifier -> ChatAvailableMessageActions in + var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:] + var banPeer: Peer? + var hadBanPeerId = false for id in messageIds { + if optionsMap[id] == nil { + optionsMap[id] = [] + } if id.peerId == accountPeerId { - optionsMap[id] = .locally + optionsMap[id]!.insert(.deleteLocally) } else if let peer = modifier.getPeer(id.peerId), let message = modifier.getMessage(id) { if let channel = peer as? TelegramChannel { - var options: ChatDeleteMessagesOptions = [] - if !message.flags.contains(.Incoming) { - options.insert(.globally) - } else { - if channel.hasAdminRights([.canDeleteMessages]) { - options.insert(.globally) + if channel.hasAdminRights(.canBanUsers), case .group = channel.info { + if message.flags.contains(.Incoming) { + if !hadBanPeerId { + hadBanPeerId = true + banPeer = message.author + } else if banPeer?.id != message.author?.id { + banPeer = nil + } + } else { + hadBanPeerId = true + banPeer = nil } } - optionsMap[message.id] = options - } else if let group = peer as? TelegramGroup { - var options: ChatDeleteMessagesOptions = [] - options.insert(.locally) + optionsMap[id]!.insert(.forward) if !message.flags.contains(.Incoming) { - options.insert(.globally) + optionsMap[id]!.insert(.deleteGlobally) + } else { + if channel.hasAdminRights([.canDeleteMessages]) { + optionsMap[id]!.insert(.deleteGlobally) + } + } + } else if let group = peer as? TelegramGroup { + optionsMap[id]!.insert(.forward) + optionsMap[id]!.insert(.deleteLocally) + if !message.flags.contains(.Incoming) { + optionsMap[id]!.insert(.deleteGlobally) } else { switch group.role { case .creator, .admin: - options.insert(.globally) + optionsMap[id]!.insert(.deleteGlobally) case .member: break } } - optionsMap[message.id] = options } else if let _ = peer as? TelegramUser { - var options: ChatDeleteMessagesOptions = [] - options.insert(.locally) + optionsMap[id]!.insert(.forward) + optionsMap[id]!.insert(.deleteLocally) if !message.flags.contains(.Incoming) { - options.insert(.globally) + optionsMap[id]!.insert(.deleteGlobally) } - optionsMap[message.id] = options } else if let _ = peer as? TelegramSecretChat { - var options: ChatDeleteMessagesOptions = [] - options.insert(.globally) - optionsMap[message.id] = options + optionsMap[id]!.insert(.deleteGlobally) } else { assertionFailure() } } else { - optionsMap[id] = [.locally] + optionsMap[id]!.insert(.deleteLocally) } } @@ -352,9 +374,9 @@ func chatDeleteMessagesOptions(postbox: Postbox, accountPeerId: PeerId, messageI for value in optionsMap.values { reducedOptions.formIntersection(value) } - return reducedOptions + return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer) } else { - return [] + return ChatAvailableMessageActions(options: [], banAuthor: nil) } } } diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 7e9a1f96f7..1789195093 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -347,8 +347,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { } private func commitDeleteMessages(_ messages: [Message], ask: Bool) { - self.messageContextDisposable.set((chatDeleteMessagesOptions(postbox: self.account.postbox, accountPeerId: self.account.peerId, messageIds: Set(messages.map { $0.id })) |> deliverOnMainQueue).start(next: { [weak self] options in - if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !options.isEmpty { + self.messageContextDisposable.set((chatAvailableMessageActions(postbox: self.account.postbox, accountPeerId: self.account.peerId, messageIds: Set(messages.map { $0.id })) |> deliverOnMainQueue).start(next: { [weak self] actions in + if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !actions.options.isEmpty { let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) var items: [ActionSheetItem] = [] var personalPeerName: String? @@ -359,7 +359,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { isChannel = true } - if options.contains(.globally) { + if actions.options.contains(.deleteGlobally) { let globalTitle: String if isChannel { globalTitle = strongSelf.strings.Common_Delete @@ -376,7 +376,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { } })) } - if options.contains(.locally) { + if actions.options.contains(.deleteLocally) { items.append(ActionSheetButtonItem(title: strongSelf.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index aac0214b26..7e868cb664 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -196,7 +196,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.chatListDisplayNode.chatListNode.presentAlert = { [weak self] text in if let strongSelf = self { - self?.present(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index ddf669ca75..cfc93ff431 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -19,13 +19,14 @@ class ChatListItem: ListViewItem { let content: ChatListItemContent let editing: Bool let hasActiveRevealControls: Bool + let enableContextActions: Bool let interaction: ChatListNodeInteraction let selectable: Bool = true let header: ListViewItemHeader? - init(presentationData: ChatListPresentationData, account: Account, peerGroupId: PeerGroupId?, index: ChatListIndex, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { + init(presentationData: ChatListPresentationData, account: Account, peerGroupId: PeerGroupId?, index: ChatListIndex, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, enableContextActions: Bool, interaction: ChatListNodeInteraction) { self.presentationData = presentationData self.peerGroupId = peerGroupId self.account = account @@ -34,6 +35,7 @@ class ChatListItem: ListViewItem { self.editing = editing self.hasActiveRevealControls = hasActiveRevealControls self.header = header + self.enableContextActions = enableContextActions self.interaction = interaction } @@ -468,7 +470,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hasDraft = true authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: embeddedState.text.string, font: textFont, textColor: theme.messageTextColor) } else if let message = message { attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) @@ -567,11 +569,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var isVerified = false let isSecret = item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat - if let peer = itemPeer.chatMainPeer { - if let peer = peer as? TelegramUser { - isVerified = peer.flags.contains(.isVerified) - } else if let peer = peer as? TelegramChannel { - isVerified = peer.flags.contains(.isVerified) + + if case .peer = item.content { + if let peer = itemPeer.chatMainPeer { + if let peer = peer as? TelegramUser { + isVerified = peer.flags.contains(.isVerified) + } else if let peer = peer as? TelegramChannel { + isVerified = peer.flags.contains(.isVerified) + } } } @@ -645,11 +650,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { isPinned = item.index.pinningIndex != nil } - peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: item.account.peerId != item.index.messageIndex.id.peerId ? (currentMutedIconImage != nil) : nil, hasPeerGroupId: hasPeerGroupId, canDelete: true) + if item.enableContextActions { + peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: item.account.peerId != item.index.messageIndex.id.peerId ? (currentMutedIconImage != nil) : nil, hasPeerGroupId: hasPeerGroupId, canDelete: true) + } else { + peerRevealOptions = [] + } case .groupReference: let isPinned = item.index.pinningIndex != nil - peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: nil, hasPeerGroupId: nil, canDelete: false) + if item.enableContextActions { + peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: nil, hasPeerGroupId: nil, canDelete: false) + } else { + peerRevealOptions = [] + } } return (layout, { [weak self] animated in diff --git a/TelegramUI/ChatListItemStrings.swift b/TelegramUI/ChatListItemStrings.swift index 24e7acf619..bcc63bb674 100644 --- a/TelegramUI/ChatListItemStrings.swift +++ b/TelegramUI/ChatListItemStrings.swift @@ -77,8 +77,12 @@ public func chatListItemStrings(strings: PresentationStrings, message: Message?, messageText = strings.Message_Animation } } - case _ as TelegramMediaMap: - messageText = strings.Message_Location + case let location as TelegramMediaMap: + if location.liveBroadcastingTimeout != nil { + messageText = strings.Message_LiveLocation + } else { + messageText = strings.Message_Location + } case _ as TelegramMediaContact: messageText = strings.Message_Contact case let game as TelegramMediaGame: diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index d6ab03900e..2e6ebc3667 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -101,7 +101,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(onlyWriteable): let itemPeer = peer.chatMainPeer var chatPeer: Peer? @@ -116,7 +116,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -125,7 +125,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode case let .HoleEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(index, presentationData, groupId, message, topPeers, counters, editing): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) } } } @@ -140,7 +140,7 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities), editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(onlyWriteable): let itemPeer = peer.chatMainPeer var chatPeer: Peer? @@ -155,7 +155,7 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -164,7 +164,7 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode case let .HoleEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(index, presentationData, groupId, message, topPeers, counters, editing): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, account: account, peerGroupId: peerGroupId, index: index, content: .groupReference(groupId: groupId, message: message, topPeers: topPeers, counters: counters), editing: editing, hasActiveRevealControls: false, header: nil, enableContextActions: true, interaction: nodeInteraction), directionHint: entry.directionHint) } } } diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index cef9e7f40f..ae482b5a9f 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -224,7 +224,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, if let savedMessagesPeer = savedMessagesPeer { result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, inputActivities: nil)) } - result.append(.SearchEntry(theme: state.presentationData.theme, text: state.presentationData.strings.DialogList_SearchLabel)) + result.append(.SearchEntry(theme: state.presentationData.theme, text: view.groupId == nil ? state.presentationData.strings.DialogList_SearchLabel : "Search this feed")) } if result.count == 2, case .SearchEntry = result[1], case .HoleEntry = result[0] { return [] diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 9b8849a79a..911b973f65 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -122,7 +122,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { clearRecentlySearchedPeers() }), action: { _ in peerSelected(peer) @@ -268,7 +268,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): @@ -288,11 +288,11 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer.peer, chatPeer: peer.peer, status: .addressName(suffixString), enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: peer.peer, chatPeer: peer.peer, status: .addressName(suffixString), enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer.peer) }) case let .message(message, presentationData): - return ChatListItem(presentationData: presentationData, account: account, peerGroupId: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), content: .peer(message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil), editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, interaction: interaction) + return ChatListItem(presentationData: presentationData, account: account, peerGroupId: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), content: .peer(message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil), editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, enableContextActions: false, interaction: interaction) } } } @@ -399,8 +399,13 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if let query = query, !query.isEmpty { let accountPeer = account.postbox.loadedPeerWithId(account.peerId) |> take(1) let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased(), groupId: groupId) - let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then(searchPeers(account: account, query: query) + let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> + if groupId == nil { + foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + } else { + foundRemotePeers = .single(([], [])) + } let location: SearchMessagesLocation if let groupId = groupId { location = .group(groupId) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index dd4726fc6f..ca84af48df 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -355,8 +355,8 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } } attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) - case let .customText(text): - attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor) + case let .customText(text, entities): + attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, fixedFont: titleFont) case .unknown: attributedString = nil } @@ -535,7 +535,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { case .instantPage: foundTapAction = true if let item = self.item { - item.controllerInteraction.openInstantPage(item.message.id) + item.controllerInteraction.openInstantPage(item.message) } case .holdToPreviewSecretMedia: foundTapAction = true @@ -588,7 +588,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } if !foundTapAction { - item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.filledBackgroundNode.frame) + item.controllerInteraction.openMessageContextMenu(item.message, self, self.filledBackgroundNode.frame) } } case .hold: diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index ab82fe01be..d764065c94 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -9,6 +9,7 @@ import Postbox private let titleFont: UIFont = Font.semibold(15.0) private let textFont: UIFont = Font.regular(15.0) private let textBoldFont: UIFont = Font.semibold(15.0) +private let textItalicFont: UIFont = Font.italic(15.0) private let textFixedFont: UIFont = Font.regular(15.0) private let buttonFont: UIFont = Font.semibold(13.0) @@ -358,7 +359,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) } if let entities = entities { - string.append(stringWithAppliedEntities(text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, boldFont: textBoldFont, fixedFont: textFixedFont)) + string.append(stringWithAppliedEntities(text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont)) } else { string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 350904a97f..05d2134094 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -65,6 +65,17 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [( result.append((item.content.firstMessage, ChatMessageTextBubbleContentNode.self)) } + if let additionalContent = item.additionalContent { + switch additionalContent { + case let .eventLogPreviousMessage(previousMessage): + result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self)) + case let .eventLogPreviousDescription(previousMessage): + result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self)) + case let .eventLogPreviousLink(previousMessage): + result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self)) + } + } + return result } @@ -1234,7 +1245,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.content.firstMessage.author { - item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, .info, item.message.id) + item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, .info, item.message) } return } @@ -1244,7 +1255,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName { item.controllerInteraction.updateInputState { textInputState in - return ChatTextInputState(inputText: "@" + addressName + " ") + return ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " ")) } return } @@ -1265,7 +1276,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let sourceMessageId = forwardInfo.sourceMessageId { item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else { - item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil) + item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil, messageId: nil), nil) } return } @@ -1282,7 +1293,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break loop case let .peerMention(peerId, _): foundTapAction = true - self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) + self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, messageId: nil), nil) break loop case let .textMention(name): foundTapAction = true @@ -1301,7 +1312,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .instantPage: foundTapAction = true if let item = self.item { - item.controllerInteraction.openInstantPage(item.message.id) + item.controllerInteraction.openInstantPage(item.message) } break loop case .holdToPreviewSecretMedia: @@ -1318,12 +1329,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .longTap, .doubleTap: if let item = self.item, self.backgroundNode.frame.contains(location) { var foundTapAction = false - var tapMessageId: MessageId? = item.content.firstMessage.id + var tapMessage: Message? = item.content.firstMessage loop: for contentNode in self.contentNodes { if !contentNode.frame.contains(location) { continue loop } - tapMessageId = contentNode.item?.message.id + tapMessage = contentNode.item?.message let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY)) switch tapAction { case .none, .ignore: @@ -1356,8 +1367,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break } } - if !foundTapAction, let tapMessageId = tapMessageId { - item.controllerInteraction.openMessageContextMenu(tapMessageId, self, self.backgroundNode.frame) + if !foundTapAction, let tapMessage = tapMessage { + item.controllerInteraction.openMessageContextMenu(tapMessage, self, self.backgroundNode.frame) } } case .hold: @@ -1602,7 +1613,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { peerId = item.message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - item.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil) + item.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil) } case .payment: item.controllerInteraction.openCheckoutOrReceipt(item.message.id) diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index a2150ce58f..ed46896942 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -246,7 +246,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { @objc func contactTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) } } } diff --git a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift new file mode 100644 index 0000000000..720f22e468 --- /dev/null +++ b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -0,0 +1,109 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { + private let contentNode: ChatMessageAttachedContentNode + + required init() { + self.contentNode = ChatMessageAttachedContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let contentNodeLayout = self.contentNode.asyncLayout() + + return { item, layoutConstants, _, _, constrainedSize in + var messageEntities: [MessageTextEntity]? + + for attribute in item.message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + break + } + } + + let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousDescription + let subtitle: String? = nil + let text: String + if item.message.text.isEmpty { + text = item.presentationData.strings.Channel_AdminLog_EmptyMessageText + } else { + text = item.message.text + } + let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil + + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) + + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) + + return (size, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + + apply(animation) + + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.bounds.contains(point) { + /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + if content.instantPage != nil { + return .instantPage + } + }*/ + } + return .none + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.contentNode.updateHiddenMedia(media) + } + + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } + return self.contentNode.transitionNode(media: media) + } +} + + diff --git a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift new file mode 100644 index 0000000000..9bfe51858f --- /dev/null +++ b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift @@ -0,0 +1,105 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { + private let contentNode: ChatMessageAttachedContentNode + + required init() { + self.contentNode = ChatMessageAttachedContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let contentNodeLayout = self.contentNode.asyncLayout() + + return { item, layoutConstants, _, _, constrainedSize in + var messageEntities: [MessageTextEntity]? + + for attribute in item.message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + break + } + } + + let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousLink + let subtitle: String? = nil + let text: String = item.message.text + let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil + + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) + + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) + + return (size, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + + apply(animation) + + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.bounds.contains(point) { + /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + if content.instantPage != nil { + return .instantPage + } + }*/ + } + return .none + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.contentNode.updateHiddenMedia(media) + } + + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } + return self.contentNode.transitionNode(media: media) + } +} + + + diff --git a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift new file mode 100644 index 0000000000..13fc1e71c2 --- /dev/null +++ b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift @@ -0,0 +1,110 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { + private let contentNode: ChatMessageAttachedContentNode + + required init() { + self.contentNode = ChatMessageAttachedContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let contentNodeLayout = self.contentNode.asyncLayout() + + return { item, layoutConstants, _, _, constrainedSize in + var messageEntities: [MessageTextEntity]? + + for attribute in item.message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + break + } + } + + let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousMessage + let subtitle: String? = nil + let text: String + if item.message.text.isEmpty { + text = item.presentationData.strings.Channel_AdminLog_EmptyMessageText + } else { + text = item.message.text + } + let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil + + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) + + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + + return (contentProperties, nil, initialWidth, { constrainedSize, position in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) + + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) + + return (size, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + + apply(animation) + + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.bounds.contains(point) { + let contentNodeFrame = self.contentNode.frame + return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) + } + return .none + } + + override func updateTouchesAtPoint(_ point: CGPoint?) { + let contentNodeFrame = self.contentNode.frame + self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.contentNode.updateHiddenMedia(media) + } + + override func transitionNode(messageId: MessageId, media: Media) -> ASDisplayNode? { + if self.item?.message.id != messageId { + return nil + } + return self.contentNode.transitionNode(media: media) + } +} + diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index b54d6cd492..ea3353611b 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -18,7 +18,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.activateLocalContent = { [weak self] in if let strongSelf = self { if let item = strongSelf.item { - item.controllerInteraction.openMessage(item.message.id) + let _ = item.controllerInteraction.openMessage(item.message) } } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index bb0de9f989..4665f7c3d0 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -483,7 +483,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if strongSelf.infoBackgroundNode.alpha.isZero { item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) } else { - let _ = item.controllerInteraction.openMessage(item.message.id) + let _ = item.controllerInteraction.openMessage(item.message) } } } @@ -560,7 +560,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.message.author { - item.controllerInteraction.openPeer(author.id, .info, item.message.id) + item.controllerInteraction.openPeer(author.id, .info, item.message) } return } @@ -581,7 +581,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if let sourceMessageId = forwardInfo.sourceMessageId { item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId) } else { - item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil) + item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil, messageId: nil), nil) } return } @@ -596,7 +596,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if self.infoBackgroundNode.alpha.isZero { item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) } else { - let _ = item.controllerInteraction.openMessage(item.message.id) + let _ = item.controllerInteraction.openMessage(item.message) } return } @@ -604,7 +604,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { self.item?.controllerInteraction.clickThroughMessage() case .longTap, .doubleTap: if let item = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) { - item.controllerInteraction.openMessageContextMenu(item.message.id, self, videoNode.frame) + item.controllerInteraction.openMessageContextMenu(item.message, self, videoNode.frame) } case .hold: break diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 8d5aa2923d..200fac2bba 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -415,6 +415,11 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if isVoice { if strongSelf.waveformScrubbingNode == nil { let waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: strongSelf.waveformNode, foregroundContentNode: strongSelf.waveformForegroundNode)) + waveformScrubbingNode.seek = { timestamp in + if let strongSelf = self, let account = strongSelf.account, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) { + account.telegramApplicationContext.mediaManager.playlistControl(.seek(timestamp), type: type) + } + } waveformScrubbingNode.status = strongSelf.playbackStatus.get() strongSelf.waveformScrubbingNode = waveformScrubbingNode strongSelf.addSubnode(waveformScrubbingNode) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 7f155dfb91..86ced435bb 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -283,7 +283,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { updateVideoFile = file if hasCurrentVideoNode { } else { - let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0), content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, enableSound: false), priority: .embedded) + let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0, nativeSize: nativeSize), content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, enableSound: false), priority: .embedded) videoNode.isUserInteractionEnabled = false updatedVideoNode = videoNode replaceVideoNode = true @@ -347,7 +347,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 8565f7307c..fcae81c08e 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -150,6 +150,12 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - } } +public enum ChatMessageItemAdditionalContent { + case eventLogPreviousMessage(Message) + case eventLogPreviousDescription(Message) + case eventLogPreviousLink(Message) +} + public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let presentationData: ChatPresentationData let account: Account @@ -158,6 +164,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let content: ChatMessageItemContent let disableDate: Bool let effectiveAuthorId: PeerId? + let additionalContent: ChatMessageItemAdditionalContent? public let accessoryItem: ListViewAccessoryItem? let header: ChatMessageDateHeader @@ -180,13 +187,14 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - public init(presentationData: ChatPresentationData, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false) { + public init(presentationData: ChatPresentationData, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false, additionalContent: ChatMessageItemAdditionalContent? = nil) { self.presentationData = presentationData self.account = account self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction self.content = content self.disableDate = disableDate + self.additionalContent = additionalContent var accessoryItem: ListViewAccessoryItem? let incoming = content.effectivelyIncoming(self.account.peerId) diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index 2d20040823..27e7b1ddf0 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -20,6 +20,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { private var media: TelegramMediaMap? + private var timeoutTimer: (SwiftSignalKit.Timer, Int32)? + required init() { self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] @@ -38,6 +40,10 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { fatalError("init(coder:) has not been implemented") } + deinit { + self.timeoutTimer?.0.invalidate() + } + override func didLoad() { super.didLoad() @@ -368,6 +374,22 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.liveTextNode?.update(color: timerTextColor, timestamp: Double(updateTimestamp), strings: item.presentationData.strings, timeFormat: item.presentationData.timeFormat) + + let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout + if strongSelf.timeoutTimer?.1 != timeoutDeadline { + strongSelf.timeoutTimer?.0.invalidate() + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: { + if let strongSelf = self { + strongSelf.timeoutTimer?.0.invalidate() + strongSelf.timeoutTimer = nil + item.controllerInteraction.requestMessageUpdate(item.message.id) + } + }, queue: Queue.mainQueue()) + strongSelf.timeoutTimer = (timer, timeoutDeadline) + timer.start() + } } else { if let liveTimerNode = strongSelf.liveTimerNode { strongSelf.liveTimerNode = nil @@ -382,6 +404,11 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { liveTextNode?.removeFromSupernode() }) } + + if let (timer, _) = strongSelf.timeoutTimer { + strongSelf.timeoutTimer = nil + timer.invalidate() + } } imageApply() @@ -436,7 +463,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) } } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index f69f181ae2..40e34ccc44 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -33,7 +33,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.activateLocalContent = { [weak self] in if let strongSelf = self { if let item = strongSelf.item, !item.message.containsSecretMedia { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) } } } diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index 3ba8d6e552..5df9d26ba5 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -51,6 +51,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private var item: ChatMessageNotificationItem? private let avatarNode: AvatarNode + private let titleIconNode: ASImageNode private let titleNode: TextNode private let textNode: TextNode private let imageNode: TransformImageNode @@ -66,6 +67,11 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.titleNode = TextNode() self.titleNode.isLayerBacked = true + self.titleIconNode = ASImageNode() + self.titleIconNode.isLayerBacked = true + self.titleIconNode.displayWithoutProcessing = true + self.titleIconNode.displaysAsynchronously = false + self.textNode = TextNode() self.textNode.isLayerBacked = true @@ -74,6 +80,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { super.init() self.addSubnode(self.avatarNode) + self.addSubnode(self.titleIconNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) @@ -95,21 +102,30 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } + var titleIcon: UIImage? + if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat { + titleIcon = PresentationResourcesRootController.inAppNotificationSecretChatIcon(presentationData.theme) + } + + self.titleIconNode.image = titleIcon + var updatedMedia: Media? var imageDimensions: CGSize? - for media in item.message.media { - if let image = media as? TelegramMediaImage { - updatedMedia = image - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions + if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat { + for media in item.message.media { + if let image = media as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions + } + break + } else if let file = media as? TelegramMediaFile { + updatedMedia = file + if let representation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = representation.dimensions + } + break } - break - } else if let file = media as? TelegramMediaFile { - updatedMedia = file - if let representation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = representation.dimensions - } - break } } @@ -133,7 +149,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } - let messageText = descriptionStringForMessage(item.message, strings: item.strings, accountPeerId: item.account.peerId) + let messageText: String + if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat { + messageText = item.strings.ENCRYPTED_MESSAGE("").0 + } else { + messageText = descriptionStringForMessage(item.message, strings: item.strings, accountPeerId: item.account.peerId) + } if let applyImage = applyImage { applyImage() @@ -166,8 +187,13 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 54.0, height: 54.0))) + var titleInset: CGFloat = 0.0 + if let image = self.titleIconNode.image { + titleInset += image.size.width + 4.0 + } + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - titleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -177,9 +203,13 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let textSpacing: CGFloat = 1.0 - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) transition.updateFrame(node: self.titleNode, frame: titleFrame) + if let image = self.titleIconNode.image { + transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: leftInset + 1.0, y: titleFrame.minY + 3.0), size: image.size)) + } + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 9.0 - 55.0, y: 9.0), size: CGSize(width: 55.0, height: 55.0))) diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index da0472e3c3..9a9f79a9b3 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -21,19 +21,17 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if oldValue != self.selectedMessages { self.forwardButton.isEnabled = self.selectedMessages.count != 0 - let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) - if self.selectedMessages.isEmpty { self.canDeleteMessagesDisposable.set(nil) self.deleteButton.isEnabled = false self.shareButton.isEnabled = false } else if let account = self.account { let isEmpty = self.selectedMessages.isEmpty - self.canDeleteMessagesDisposable.set((chatDeleteMessagesOptions(postbox: account.postbox, accountPeerId: account.peerId, messageIds: self.selectedMessages) - |> deliverOnMainQueue).start(next: { [weak self] options in + self.canDeleteMessagesDisposable.set((chatAvailableMessageActions(postbox: account.postbox, accountPeerId: account.peerId, messageIds: self.selectedMessages) + |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self { - strongSelf.deleteButton.isEnabled = !options.isEmpty - strongSelf.shareButton.isEnabled = !isEmpty + strongSelf.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty + strongSelf.shareButton.isEnabled = !actions.options.intersection([.forward]).isEmpty } })) } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 6987c498ee..c2fc2e44a7 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -251,7 +251,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.message.author { - item.controllerInteraction.openPeer(author.id, .info, item.message.id) + item.controllerInteraction.openPeer(author.id, .info, item.message) } return } @@ -291,14 +291,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { }*/ if let item = self.item, self.imageNode.frame.contains(location) { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) return } self.item?.controllerInteraction.clickThroughMessage() case .longTap, .doubleTap: if let item = self.item, self.imageNode.frame.contains(location) { - item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.imageNode.frame) + item.controllerInteraction.openMessageContextMenu(item.message, self, self.imageNode.frame) } case .hold: break diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 8f26f7a521..900c74f960 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -164,7 +164,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let bubbleTheme = item.presentationData.theme.chat.bubble if let entities = entities { - attributedText = stringWithAppliedEntities(message.text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: item.presentationData.messageFont, boldFont: item.presentationData.messageBoldFont, fixedFont: item.presentationData.messageFixedFont) + attributedText = stringWithAppliedEntities(message.text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: item.presentationData.messageFont, linkFont: item.presentationData.messageFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, fixedFont: item.presentationData.messageFixedFont) } else { attributedText = NSAttributedString(string: message.text, font: item.presentationData.messageFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor) } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 3b0df855c7..4d922a1fcf 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -102,7 +102,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.contentNode) self.contentNode.openMedia = { [weak self] in if let strongSelf = self, let item = strongSelf.item { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) } } self.contentNode.activateAction = { [weak self] in diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index d75727fe17..6381d3983c 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -73,9 +73,10 @@ final class ChatPanelInterfaceInteraction { let toggleMessageStickerStarred: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void let navigateFeed: () -> Void + let openGrouping: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -117,6 +118,7 @@ final class ChatPanelInterfaceInteraction { self.toggleMessageStickerStarred = toggleMessageStickerStarred self.presentController = presentController self.navigateFeed = navigateFeed + self.openGrouping = openGrouping self.statuses = statuses } } diff --git a/TelegramUI/ChatPresentationData.swift b/TelegramUI/ChatPresentationData.swift index b2b971f5bd..3855de664d 100644 --- a/TelegramUI/ChatPresentationData.swift +++ b/TelegramUI/ChatPresentationData.swift @@ -26,6 +26,7 @@ public final class ChatPresentationData { let messageFont: UIFont let messageBoldFont: UIFont + let messageItalicFont: UIFont let messageFixedFont: UIFont init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, wallpaper: TelegramWallpaper, timeFormat: PresentationTimeFormat) { @@ -38,6 +39,7 @@ public final class ChatPresentationData { let baseFontSize = fontSize.baseDisplaySize self.messageFont = UIFont.systemFont(ofSize: baseFontSize) self.messageBoldFont = UIFont.boldSystemFont(ofSize: baseFontSize) + self.messageItalicFont = UIFont.italicSystemFont(ofSize: baseFontSize) self.messageFixedFont = UIFont(name: "Menlo-Regular", size: baseFontSize - 1.0) ?? UIFont.systemFont(ofSize: baseFontSize) } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 5478ece8cf..f17f8b6f3e 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -256,11 +256,11 @@ enum ChatTitlePanelContext: Comparable { } struct ChatSearchResultsState: Equatable { - let messageIds: [MessageId] + let messageIndices: [MessageIndex] let currentId: MessageId? static func ==(lhs: ChatSearchResultsState, rhs: ChatSearchResultsState) -> Bool { - if lhs.messageIds != rhs.messageIds { + if lhs.messageIndices != rhs.messageIndices { return false } if lhs.currentId != rhs.currentId { diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift new file mode 100644 index 0000000000..01b9629f67 --- /dev/null +++ b/TelegramUI/ChatRecentActionsController.swift @@ -0,0 +1,210 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class ChatRecentActionsController: ViewController { + private var controllerNode: ChatRecentActionsControllerNode { + return self.displayNode as! ChatRecentActionsControllerNode + } + + private let account: Account + private let peer: Peer + private var presentationData: PresentationData + + private var interaction: ChatRecentActionsInteraction! + private var panelInteraction: ChatPanelInterfaceInteraction! + + private let titleView: ChatRecentActionsTitleView + + init(account: Account, peer: Peer) { + self.account = account + self.peer = peer + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.titleView = ChatRecentActionsTitleView(color: self.presentationData.theme.rootController.navigationBar.primaryTextColor) + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.interaction = ChatRecentActionsInteraction(displayInfoAlert: { [weak self] in + if let strongSelf = self { + self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertTitle, text: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + + self.panelInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in + }, setupEditMessage: { _ in + }, beginMessageSelection: { _ in + }, deleteSelectedMessages: { + }, forwardSelectedMessages: { [weak self] in + /*if let strongSelf = self { + if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + + let controller = PeerSelectionController(account: strongSelf.account) + controller.peerSelected = { [weak controller] peerId in + if let strongSelf = self, let _ = controller { + let _ = (strongSelf.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(forwardMessageIds) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) + } + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.updateInterfaceState(animated: false, { $0.withoutSelectionState() }) + + let ready = ValuePromise() + + strongSelf.messageContextDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let strongController = controller { + strongController.dismiss() + } + })) + + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) + } + }) + } + } + strongSelf.present(controller, in: .window(.root)) + } + }*/ + }, shareSelectedMessages: { [weak self] in + /*if let strongSelf = self, let selectedIds = strongSelf.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { + let _ = (strongSelf.account.postbox.modify { modifier -> [Message] in + var messages: [Message] = [] + for id in selectedIds { + if let message = modifier.getMessage(id) { + messages.append(message) + } + } + return messages + } |> deliverOnMainQueue).start(next: { messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.updateInterfaceState(animated: true, { + $0.withoutSelectionState() + }) + + let shareController = ShareController(account: strongSelf.account, subject: .messages(messages.sorted(by: { lhs, rhs in + return MessageIndex(lhs) < MessageIndex(rhs) + })), externalShare: true, immediateExternalShare: true) + strongSelf.present(shareController, in: .window(.root)) + } + }) + }*/ + }, updateTextInputState: { _ in + }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in + }, editMessage: { + }, beginMessageSearch: { _ in + }, dismissMessageSearch: { + }, updateMessageSearch: { _ in + }, navigateMessageSearch: { _ in + }, openCalendarSearch: { + }, toggleMembersSearch: { _ in + }, navigateToMessage: { _ in + }, openPeerInfo: { + }, togglePeerNotifications: { + }, sendContextResult: { _, _ in + }, sendBotCommand: { _, _ in + }, sendBotStart: { _ in + }, botSwitchChatWithPayload: { _, _ in + }, beginMediaRecording: { _ in + }, finishMediaRecording: { _ in + }, stopMediaRecording: { + }, lockMediaRecording: { + }, deleteRecordedMedia: { + }, sendRecordedMedia: { + }, switchMediaRecordingMode: { + }, setupMessageAutoremoveTimeout: { + }, sendSticker: { _ in + }, unblockPeer: { + }, pinMessage: { _ in + }, unpinMessage: { + }, reportPeer: { + }, dismissReportPeer: { + }, deleteChat: { + }, beginCall: { + }, toggleMessageStickerStarred: { _ in + }, presentController: { _, _ in + }, navigateFeed: { + }, openGrouping: { + }, statuses: nil) + + self.navigationItem.titleView = self.titleView + + let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false) + + self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents + self.titleView.pressed = { [weak self] in + self?.openFilterSetup() + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = ChatRecentActionsControllerNode(account: self.account, peer: self.peer, presentationData: self.presentationData, interaction: self.interaction, pushController: { [weak self] c in + (self?.navigationController as? NavigationController)?.pushViewController(c) + }, presentController: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, getNavigationController: { [weak self] in + return self?.navigationController as? NavigationController + }) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func activateSearch() { + if let navigationBar = self.navigationBar { + if !(navigationBar.contentNode is ChatRecentActionsSearchNavigationContentNode) { + let searchNavigationNode = ChatRecentActionsSearchNavigationContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, cancel: { [weak self] in + self?.deactivateSearch() + }) + + navigationBar.setContentNode(searchNavigationNode, animated: true) + searchNavigationNode.setQueryUpdated({ [weak self] query in + self?.controllerNode.updateSearchQuery(query) + self?.updateTitle() + }) + searchNavigationNode.activate() + } + } + } + + private func deactivateSearch() { + self.controllerNode.updateSearchQuery("") + self.navigationBar?.setContentNode(nil, animated: true) + self.updateTitle() + } + + private func openFilterSetup() { + self.present(channelRecentActionsFilterController(account: self.account, peer: self.peer, events: self.controllerNode.filter.events, adminPeerIds: self.controllerNode.filter.adminPeerIds, apply: { [weak self] events, adminPeerIds in + self?.controllerNode.updateFilter(events: events, adminPeerIds: adminPeerIds) + self?.updateTitle() + }), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + + private func updateTitle() { + if self.controllerNode.filter.isEmpty { + self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents + } else { + self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleSelectedEvents + } + } +} diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift new file mode 100644 index 0000000000..665c47faae --- /dev/null +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -0,0 +1,636 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +import SafariServices + +private final class ChatRecentActionsListOpaqueState { + let entries: [ChatRecentActionsEntry] + let canLoadEarlier: Bool + + init(entries: [ChatRecentActionsEntry], canLoadEarlier: Bool) { + self.entries = entries + self.canLoadEarlier = canLoadEarlier + } +} + +final class ChatRecentActionsControllerNode: ViewControllerTracingNode { + private let account: Account + private let peer: Peer + private var presentationData: PresentationData + + private let pushController: (ViewController) -> Void + private let presentController: (ViewController, Any?) -> Void + private let getNavigationController: () -> NavigationController? + + private let interaction: ChatRecentActionsInteraction + private var controllerInteraction: ChatControllerInteraction! + + private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() + + private var chatPresentationDataPromise: Promise + private var presentationDataDisposable: Disposable? + + private var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + + private var state: ChatRecentActionsControllerState + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let backgroundNode: ASDisplayNode + private let panelBackgroundNode: ASDisplayNode + private let panelSeparatorNode: ASDisplayNode + private let panelButtonNode: HighlightableButtonNode + + private let listNode: ListView + private let loadingNode: ChatLoadingNode + private let emptyNode: ChatRecentActionsEmptyNode + + private let navigationActionDisposable = MetaDisposable() + + private var isLoading: Bool = false { + didSet { + if self.isLoading != oldValue { + if self.isLoading { + self.listNode.supernode?.insertSubnode(self.loadingNode, aboveSubnode: self.listNode) + } else { + self.loadingNode.removeFromSupernode() + } + } + } + } + + private(set) var filter: ChannelAdminEventLogFilter = ChannelAdminEventLogFilter() + private let context: ChannelAdminEventLogContext + + private var enqueuedTransitions: [(ChatRecentActionsHistoryTransition, Bool)] = [] + + private var historyDisposable: Disposable? + + init(account: Account, peer: Peer, presentationData: PresentationData, interaction: ChatRecentActionsInteraction, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { + self.account = account + self.peer = peer + self.presentationData = presentationData + self.interaction = interaction + self.pushController = pushController + self.presentController = presentController + self.getNavigationController = getNavigationController + + self.automaticMediaDownloadSettings = (account.applicationContext as! TelegramApplicationContext).currentAutomaticMediaDownloadSettings.with { $0 } + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.panelBackgroundNode = ASDisplayNode() + self.panelBackgroundNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor + self.panelSeparatorNode = ASDisplayNode() + self.panelSeparatorNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelStrokeColor + self.panelButtonNode = HighlightableButtonNode() + self.panelButtonNode.setTitle(self.presentationData.strings.Channel_AdminLog_InfoPanelTitle, with: Font.regular(17.0), with: self.presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) + self.loadingNode = ChatLoadingNode(theme: self.presentationData.theme) + self.emptyNode = ChatRecentActionsEmptyNode(theme: self.presentationData.theme) + self.emptyNode.isHidden = true + + self.state = ChatRecentActionsControllerState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize) + + self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.fontSize, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, timeFormat: self.presentationData.timeFormat)) + + self.context = ChannelAdminEventLogContext(postbox: self.account.postbox, network: self.account.network, peerId: self.peer.id) + + super.init() + + self.backgroundNode.contents = chatControllerBackgroundImage(wallpaper: self.state.chatWallpaper, postbox: account.postbox)?.cgImage + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.listNode) + self.addSubnode(self.emptyNode) + self.addSubnode(self.panelBackgroundNode) + self.addSubnode(self.panelSeparatorNode) + self.addSubnode(self.panelButtonNode) + + self.panelButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) + + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { + return openChatMessage(account: account, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: { + //self?.chatDisplayNode.dismissInput() + }, present: { c, a in + self?.presentController(c, a) + }, transitionNode: { messageId, media in + var selectedNode: ASDisplayNode? + if let strongSelf = self { + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + selectedNode = result + } + } + } + } + return selectedNode + }, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.listNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.listNode.view) + } + }, openUrl: { url in + self?.openUrl(url) + }, openPeer: { peer, navigation in + self?.openPeer(peerId: peer.id, peer: peer) + }, callPeer: { peerId in + self?.controllerInteraction?.callPeer(peerId) + }, sendSticker: { file in + self?.controllerInteraction?.sendSticker(file) + }, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in + if let strongSelf = self { + /*strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + if let entry = entry, entry.index == centralIndex { + messageIdAndMedia[message.id] = [galleryMedia] + } + + controllerInteraction.hiddenMedia = messageIdAndMedia + + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() + } + } + } + }))*/ + } + }) + } + return false + }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { [weak self] peerId, _, message in + if let peerId = peerId { + self?.openPeer(peerId: peerId, peer: message?.peers[peerId]) + } + }, openPeerMention: { [weak self] name in + self?.openPeerMention(name) + }, openMessageContextMenu: { [weak self] message, node, frame in + self?.openMessageContextMenu(message: message, node: node, frame: frame) + }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { [weak self] url in + self?.openUrl(url) + }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message in + if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { + openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) + } + }, openHashtag: { [weak self] peerName, hashtag in + if let strongSelf = self, !hashtag.isEmpty { + let searchController = HashtagSearchController(account: strongSelf.account, peerName: peerName, query: hashtag) + strongSelf.pushController(searchController) + } + }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in + if let strongSelf = self { + switch action { + case let .url(url): + var cleanUrl = url + var canAddToReadingList = true + let mailtoString = "mailto:" + let telString = "tel:" + var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen + if cleanUrl.hasPrefix(mailtoString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) + } else if cleanUrl.hasPrefix(telString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) + openText = strongSelf.presentationData.strings.Conversation_Call + } + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: cleanUrl)) + items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openUrl(url) + } + })) + items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = cleanUrl + })) + if canAddToReadingList { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.presentController(actionSheet, nil) + case let .peerMention(peerId, mention): + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + var items: [ActionSheetItem] = [] + if !mention.isEmpty { + items.append(ActionSheetTextItem(title: mention)) + } + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openPeer(peerId: peerId, peer: nil) + } + })) + if !mention.isEmpty { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.presentController(actionSheet, nil) + case let .mention(mention): + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openPeerMention(mention) + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.presentController(actionSheet, nil) + case let .command(command): + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: command), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = command + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.presentController(actionSheet, nil) + case let .hashtag(hashtag): + let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: hashtag), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let searchController = HashtagSearchController(account: strongSelf.account, peerName: nil, query: hashtag) + strongSelf.pushController(searchController) + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = hashtag + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.presentController(actionSheet, nil) + } + } + }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, canSetupReply: { + return false + }, requestMessageUpdate: { _ in + }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) + self.controllerInteraction = controllerInteraction + + self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in + if let strongSelf = self { + if let state = (opaqueTransactionState as? ChatRecentActionsListOpaqueState), state.canLoadEarlier { + if let visible = displayedRange.visibleRange { + let indexRange = (state.entries.count - 1 - visible.lastIndex, state.entries.count - 1 - visible.firstIndex) + if indexRange.0 < 5 { + strongSelf.context.loadMoreEntries() + } + } + } + } + } + + self.context.loadMoreEntries() + + let historyViewUpdate = self.context.get() + + let previousView = Atomic<[ChatRecentActionsEntry]?>(value: nil) + + let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get()) + |> mapToQueue { update, chatPresentationData -> Signal in + let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData) + let previous = previousView.swap(processedView) + + var prepareOnMainQueue = false + + if let previous = previous, previous == processedView { + + } else { + + } + + return .single(chatRecentActionsHistoryPreparedTransition(from: previous ?? [], to: processedView, type: update.2, canLoadEarlier: update.1, displayingResults: !processedView.isEmpty || update.1, account: account, peer: peer, controllerInteraction: controllerInteraction)) + + //return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + } + + let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in + if let strongSelf = self { + strongSelf.enqueueTransition(transition: transition, firstTime: false) + } + return .complete() + } + + self.historyDisposable = appliedTransition.start() + + self.galleryHiddenMesageAndMediaDisposable.set(self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + var messageIdAndMedia: [MessageId: [Media]] = [:] + + for id in ids { + if case let .chat(messageId, media) = id { + messageIdAndMedia[messageId] = [media] + } + } + + //if controllerInteraction.hiddenMedia != messageIdAndMedia { + controllerInteraction.hiddenMedia = messageIdAndMedia + + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() + } + } + //} + } + })) + } + + deinit { + self.historyDisposable?.dispose() + self.navigationActionDisposable.dispose() + self.galleryHiddenMesageAndMediaDisposable.dispose() + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.containerLayout == nil + + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + let cleanInsets = layout.insets(options: []) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let intrinsicPanelHeight: CGFloat = 47.0 + let panelHeight = intrinsicPanelHeight + cleanInsets.bottom + transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: intrinsicPanelHeight))) + + transition.updateBounds(node: self.listNode, bounds: CGRect(origin: CGPoint(), size: layout.size)) + transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center) + + transition.updateFrame(node: self.loadingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let emptyFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - panelHeight)) + transition.updateFrame(node: self.emptyNode, frame: emptyFrame) + self.emptyNode.updateLayout(size: emptyFrame.size, transition: transition) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let contentBottomInset: CGFloat = panelHeight + 4.0 + let listInsets = UIEdgeInsets(top: contentBottomInset, left: layout.safeInsets.right, bottom: insets.top, right: layout.safeInsets.left) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if isFirstLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(transition: ChatRecentActionsHistoryTransition, firstTime: Bool) { + self.enqueuedTransitions.append((transition, firstTime)) + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + while true { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.LowLatency) + } else { + switch transition.type { + case .initial: + options.insert(.LowLatency) + case .generic: + options.insert(.AnimateInsertion) + case .load: + break + } + } + + let isEmpty = transition.filteredEntries.isEmpty + let displayingResults = transition.displayingResults + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: ChatRecentActionsListOpaqueState(entries: transition.filteredEntries, canLoadEarlier: transition.canLoadEarlier), completion: { [weak self] _ in + if let strongSelf = self { + if displayingResults != !strongSelf.listNode.isHidden { + strongSelf.listNode.isHidden = !displayingResults + strongSelf.backgroundColor = displayingResults ? strongSelf.presentationData.theme.list.plainBackgroundColor : nil + + strongSelf.emptyNode.isHidden = displayingResults + if !displayingResults { + var text: String = "" + if let query = strongSelf.filter.query { + text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterQueryText(query).0 + } else { + text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterText + } + strongSelf.emptyNode.setup(title: strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterTitle, text: text) + } + //strongSelf.isLoading = isEmpty && !displayingResults + } + } + }) + } else { + break + } + } + } + + @objc func infoButtonPressed() { + self.interaction.displayInfoAlert() + } + + func updateSearchQuery(_ query: String) { + self.filter = self.filter.withQuery(query.isEmpty ? nil : query) + self.context.setFilter(self.filter) + } + + func updateFilter(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { + self.filter = self.filter.withEvents(events).withAdminPeerIds(adminPeerIds) + self.context.setFilter(self.filter) + } + + private func openPeer(peerId: PeerId, peer: Peer?) { + let peerSignal: Signal + if let peer = peer { + peerSignal = .single(peer) + } else { + peerSignal = self.account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) } + } + self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer = peer { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + strongSelf.pushController(infoController) + } + } + })) + } + + private func openPeerMention(_ name: String) { + let postbox = self.account.postbox + self.navigationActionDisposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) + |> take(1) + |> mapToSignal { peerId -> Signal in + if let peerId = peerId { + return postbox.loadedPeerWithId(peerId) |> map(Optional.init) + } else { + return .single(nil) + } + } + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self { + if let peer = peer { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + strongSelf.pushController(infoController) + } + } + } + })) + } + + private func openMessageContextMenu(message: Message, node: ASDisplayNode, frame: CGRect) { + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = message.text + })) + + if !actions.isEmpty { + let contextMenuController = ContextMenuController(actions: actions) + + self.controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) + self.updateItemNodesHighlightedStates(animated: true) + + contextMenuController.dismissed = { [weak self] in + if let strongSelf = self { + if strongSelf.controllerInteraction.highlightedState?.messageStableId == message.stableId { + strongSelf.controllerInteraction.highlightedState = nil + strongSelf.updateItemNodesHighlightedStates(animated: true) + } + } + } + + self.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in + if let node = node { + return (node, frame) + } else { + return nil + } + })) + } + } + + private func updateItemNodesHighlightedStates(animated: Bool) { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHighlightedState(animated: animated) + } + } + } + + private func openUrl(_ url: String) { + self.navigationActionDisposable.set((resolveUrl(account: self.account, url: url) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case let .externalUrl(url): + if let navigationController = strongSelf.getNavigationController() { + openExternalUrl(url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: navigationController) + } + case let .peer(peerId): + strongSelf.openPeer(peerId: peerId, peer: nil) + case let .botStart(peerId, payload): + break + //strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)), fromMessage: nil) + case let .groupBotStart(peerId, payload): + break + case let .channelMessage(peerId, messageId): + if let navigationController = strongSelf.getNavigationController() { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId) + } + case let .stickerPack(name): + strongSelf.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name)), nil) + case let .instantView(webpage, anchor): + strongSelf.pushController(InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor)) + case let .join(link): + strongSelf.presentController(JoinLinkPreviewController(account: strongSelf.account, link: link, navigateToPeer: { peerId in + if let strongSelf = self { + strongSelf.openPeer(peerId: peerId, peer: nil) + } + }), nil) + } + } + })) + } +} diff --git a/TelegramUI/ChatRecentActionsControllerState.swift b/TelegramUI/ChatRecentActionsControllerState.swift new file mode 100644 index 0000000000..1d7f030b20 --- /dev/null +++ b/TelegramUI/ChatRecentActionsControllerState.swift @@ -0,0 +1,32 @@ +import Foundation +import TelegramCore + +final class ChatRecentActionsControllerState: Equatable { + let chatWallpaper: TelegramWallpaper + let theme: PresentationTheme + let strings: PresentationStrings + let fontSize: PresentationFontSize + + init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { + self.chatWallpaper = chatWallpaper + self.theme = theme + self.strings = strings + self.fontSize = fontSize + } + + static func ==(lhs: ChatRecentActionsControllerState, rhs: ChatRecentActionsControllerState) -> Bool { + if lhs.chatWallpaper != rhs.chatWallpaper { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + return true + } +} diff --git a/TelegramUI/ChatRecentActionsEmptyNode.swift b/TelegramUI/ChatRecentActionsEmptyNode.swift new file mode 100644 index 0000000000..bb53119547 --- /dev/null +++ b/TelegramUI/ChatRecentActionsEmptyNode.swift @@ -0,0 +1,74 @@ +import Foundation +import Display +import AsyncDisplayKit + +private let titleFont = Font.medium(16.0) +private let textFont = Font.regular(15.0) + +final class ChatRecentActionsEmptyNode: ASDisplayNode { + private var theme: PresentationTheme + + private let backgroundNode: ASImageNode + private let titleNode: TextNode + private let textNode: TextNode + + private var layoutParams: CGSize? + + private var title: String = "" + private var text: String = "" + + init(theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isLayerBacked = true + + super.init() + + self.backgroundNode.image = PresentationResourcesChat.chatEmptyItemBackgroundImage(self.theme) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.layoutParams = size + + let spacing: CGFloat = 5.0 + let insets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + + let maxTextWidth = size.width - insets.left - insets.right - 18.0 * 2.0 + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: titleFont, textColor: self.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: self.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: insets.top + insets.bottom + titleLayout.size.height + spacing + textLayout.size.height) + let backgroundFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - titleLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top), size: titleLayout.size)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - textLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top + titleLayout.size.height + spacing), size: textLayout.size)) + + let _ = titleApply() + let _ = textApply() + } + + func setup(title: String, text: String) { + if self.title != title || self.text != text { + self.title = title + self.text = text + if let size = self.layoutParams { + self.updateLayout(size: size, transition: .immediate) + } + } + } +} diff --git a/TelegramUI/ChatRecentActionsFilterController.swift b/TelegramUI/ChatRecentActionsFilterController.swift new file mode 100644 index 0000000000..32c235d758 --- /dev/null +++ b/TelegramUI/ChatRecentActionsFilterController.swift @@ -0,0 +1,468 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ChatRecentActionsFilterControllerArguments { + let account: Account + + let toggleAllActions: () -> Void + let toggleAction: ([AdminLogEventsFlags]) -> Void + let toggleAllAdmins: () -> Void + let toggleAdmin: (PeerId) -> Void + + init(account: Account, toggleAllActions: @escaping () -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping () -> Void, toggleAdmin: @escaping (PeerId) -> Void) { + self.account = account + self.toggleAllActions = toggleAllActions + self.toggleAction = toggleAction + self.toggleAllAdmins = toggleAllAdmins + self.toggleAdmin = toggleAdmin + } +} + +private enum ChatRecentActionsFilterSection: Int32 { + case actions + case admins +} + +private enum ChatRecentActionsFilterEntryStableId: 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: ChatRecentActionsFilterEntryStableId, rhs: ChatRecentActionsFilterEntryStableId) -> 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 ChatRecentActionsFilterEntry: ItemListNodeEntry { + case actionsTitle(PresentationTheme, String) + case allActions(PresentationTheme, String, Bool) + case actionItem(PresentationTheme, Int32, [AdminLogEventsFlags], String, Bool) + + case adminsTitle(PresentationTheme, String) + case allAdmins(PresentationTheme, String, Bool) + case adminPeerItem(PresentationTheme, PresentationStrings, Int32, RenderedChannelParticipant, Bool) + + var section: ItemListSectionId { + switch self { + case .actionsTitle, .allActions, .actionItem: + return ChatRecentActionsFilterSection.actions.rawValue + case .adminsTitle, .allAdmins, .adminPeerItem: + return ChatRecentActionsFilterSection.admins.rawValue + } + } + + var stableId: ChatRecentActionsFilterEntryStableId { + switch self { + case .actionsTitle: + return .index(0) + case .allActions: + return .index(1) + case let .actionItem(_, index, _, _, _): + return .index(100 + index) + case .adminsTitle: + return .index(200) + case .allAdmins: + return .index(201) + case let .adminPeerItem(_, _, _, participant, _): + return .peer(participant.peer.id) + } + } + + static func ==(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { + switch lhs { + case let .actionsTitle(lhsTheme, lhsText): + if case let .actionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .allActions(lhsTheme, lhsText, lhsValue): + if case let .allActions(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .actionItem(lhsTheme, lhsIndex, lhsFlags, lhsText, lhsValue): + if case let .actionItem(rhsTheme, rhsIndex, rhsFlags, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsFlags == rhsFlags, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .adminsTitle(lhsTheme, lhsText): + if case let .adminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .allAdmins(lhsTheme, lhsText, lhsValue): + if case let .allAdmins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .adminPeerItem(lhsTheme, lhsStrings, lhsIndex, lhsParticipant, lhsChecked): + if case let .adminPeerItem(rhsTheme, rhsStrings, rhsIndex, rhsParticipant, rhsChecked) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsIndex != rhsIndex { + return false + } + if lhsParticipant != rhsParticipant { + return false + } + if lhsChecked != rhsChecked { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { + switch lhs { + case .actionsTitle: + return true + case .allActions: + switch rhs { + case .actionsTitle: + return false + default: + return true + } + case let .actionItem(_, lhsIndex, _, _, _): + switch rhs { + case .actionsTitle, .allActions: + return false + case let .actionItem(_, rhsIndex, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .adminsTitle: + switch rhs { + case .adminPeerItem, .allAdmins: + return true + default: + return false + } + case .allAdmins: + switch rhs { + case .adminPeerItem: + return true + default: + return false + } + case let .adminPeerItem(_, _, lhsIndex, _, _): + switch rhs { + case let .adminPeerItem(_, _, rhsIndex, _, _): + return lhsIndex < rhsIndex + default: + return false + } + } + } + + func item(_ arguments: ChatRecentActionsFilterControllerArguments) -> ListViewItem { + switch self { + case let .actionsTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .allActions(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleAllActions() + }) + case let .actionItem(theme, _, events, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.toggleAction(events) + }) + case let .adminsTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .allAdmins(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleAllAdmins() + }) + case let .adminPeerItem(theme, strings, _, participant, checked): + let peerText: String + switch participant.participant { + case .creator: + peerText = strings.Channel_Management_LabelCreator + case .member: + peerText = strings.ChatAdmins_AdminLabel.capitalized + } + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, sectionId: self.section, action: { + arguments.toggleAdmin(participant.peer.id) + }, setPeerIdWithRevealedOptions: { _, _ in + }, removePeer: { _ in }) + } + } +} + +private struct ChatRecentActionsFilterControllerState: Equatable { + let events: AdminLogEventsFlags + let adminPeerIds: [PeerId]? + + init(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { + self.events = events + self.adminPeerIds = adminPeerIds + } + + static func ==(lhs: ChatRecentActionsFilterControllerState, rhs: ChatRecentActionsFilterControllerState) -> Bool { + if lhs.events != rhs.events { + return false + } + if let lhsAdminPeerIds = lhs.adminPeerIds, let rhsAdminPeerIds = rhs.adminPeerIds { + if lhsAdminPeerIds != rhsAdminPeerIds { + return false + } + } else if (lhs.adminPeerIds != nil) != (rhs.adminPeerIds != nil) { + return false + } + + return true + } + + func withUpdatedEvents(_ events: AdminLogEventsFlags) -> ChatRecentActionsFilterControllerState { + return ChatRecentActionsFilterControllerState(events: events, adminPeerIds: self.adminPeerIds) + } + + func withUpdatedAdminPeerIds(_ adminPeerIds: [PeerId]?) -> ChatRecentActionsFilterControllerState { + return ChatRecentActionsFilterControllerState(events: self.events, adminPeerIds: adminPeerIds) + } +} + +private func channelRecentActionsFilterControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, peer: Peer, state: ChatRecentActionsFilterControllerState, participants: [RenderedChannelParticipant]?) -> [ChatRecentActionsFilterEntry] { + var isGroup = true + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + isGroup = false + } + + var entries: [ChatRecentActionsFilterEntry] = [] + + let order: [([AdminLogEventsFlags], String)] + if isGroup { + order = [ + ([.ban, .unban], presentationData.strings.Channel_AdminLogFilter_EventsRestrictions), + ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), + ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), + ([.info], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), + ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), + ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), + ([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned), + ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), + ] + } else { + order = [ + ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), + ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), + ([.info], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), + ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), + ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), + ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), + ] + } + + var allTypesSelected = true + outer: for (events, _) in order { + for event in events { + if !state.events.contains(event) { + allTypesSelected = false + break outer + } + } + } + + entries.append(.actionsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsTitle)) + entries.append(.allActions(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsAll, allTypesSelected)) + + var index: Int32 = 0 + for (events, text) in order { + var eventsSelected = true + inner: for event in events { + if !state.events.contains(event) { + eventsSelected = false + break inner + } + } + entries.append(.actionItem(presentationData.theme, index, events, text, eventsSelected)) + index += 1 + } + + if let participants = participants { + var allAdminsSelected = true + if let adminPeerIds = state.adminPeerIds { + for participant in participants { + if !adminPeerIds.contains(participant.peer.id) { + allAdminsSelected = false + break + } + } + } else { + allAdminsSelected = true + } + + entries.append(.adminsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsTitle)) + entries.append(.allAdmins(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsAll, allAdminsSelected)) + + var index: Int32 = 0 + for participant in participants { + var adminSelected = true + if let adminPeerIds = state.adminPeerIds { + if !adminPeerIds.contains(participant.peer.id) { + adminSelected = false + } + } else { + adminSelected = true + } + entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, index, participant, adminSelected)) + index += 1 + } + } + + return entries +} + +public func channelRecentActionsFilterController(account: Account, peer: Peer, events: AdminLogEventsFlags, adminPeerIds: [PeerId]?, apply: @escaping (_ events: AdminLogEventsFlags, _ adminPeerIds: [PeerId]?) -> Void) -> ViewController { + let statePromise = ValuePromise(ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds), ignoreRepeated: true) + let stateValue = Atomic(value: ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds)) + let updateState: ((ChatRecentActionsFilterControllerState) -> ChatRecentActionsFilterControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + + let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + + let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData + + let arguments = ChatRecentActionsFilterControllerArguments(account: account, toggleAllActions: { + updateState { current in + if current.events.isEmpty { + return current.withUpdatedEvents(.all) + } else { + return current.withUpdatedEvents([]) + } + } + }, toggleAction: { events in + if let first = events.first { + updateState { current in + var updatedEvents = current.events + if updatedEvents.contains(first) { + for event in events { + updatedEvents.remove(event) + } + } else { + for event in events { + updatedEvents.insert(event) + } + } + return current.withUpdatedEvents(updatedEvents) + } + } + }, toggleAllAdmins: { + let _ = (adminsPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { admins in + if let _ = admins { + updateState { current in + if let _ = current.adminPeerIds { + return current.withUpdatedAdminPeerIds(nil) + } else { + return current.withUpdatedAdminPeerIds([]) + } + } + } + }) + }, toggleAdmin: { adminId in + let _ = (adminsPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { admins in + if let admins = admins { + updateState { current in + if let adminPeerIds = current.adminPeerIds, let index = adminPeerIds.index(of: adminId) { + var updatedAdminPeerIds = adminPeerIds + updatedAdminPeerIds.remove(at: index) + return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) + } else { + var updatedAdminPeerIds = current.adminPeerIds ?? admins.map { $0.peer.id } + if !updatedAdminPeerIds.contains(adminId) { + updatedAdminPeerIds.append(adminId) + } + return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) + } + } + } + }) + }) + + let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peer.id) |> map { Optional($0) }) + + adminsPromise.set(adminsSignal) + + var previousPeers: [RenderedChannelParticipant]? + + let signal = combineLatest(presentationDataSignal, statePromise.get(), adminsPromise.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, admins -> (ItemListControllerState, (ItemListNodeState, ChatRecentActionsFilterEntry.ItemGenerationArguments)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let doneEnabled = !state.events.isEmpty + + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { + var resultState: ChatRecentActionsFilterControllerState? + updateState { current in + resultState = current + return current + } + if let resultState = resultState { + apply(resultState.events, resultState.adminPeerIds) + } + dismissImpl?() + }) + + let previous = previousPeers + previousPeers = admins + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, peer: peer, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return controller +} + diff --git a/TelegramUI/ChatRecentActionsHistoryTransition.swift b/TelegramUI/ChatRecentActionsHistoryTransition.swift new file mode 100644 index 0000000000..34c73e543a --- /dev/null +++ b/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -0,0 +1,714 @@ +import Foundation +import Display +import TelegramCore +import Postbox + +enum ChatRecentActionsEntryContentIndex: Int32 { + case header = 0 + case content = 1 +} + +struct ChatRecentActionsEntryId: Hashable, Comparable { + let eventId: AdminLogEventId + let contentIndex: ChatRecentActionsEntryContentIndex + + static func ==(lhs: ChatRecentActionsEntryId, rhs: ChatRecentActionsEntryId) -> Bool { + return lhs.eventId == rhs.eventId && lhs.contentIndex == rhs.contentIndex + } + + static func <(lhs: ChatRecentActionsEntryId, rhs: ChatRecentActionsEntryId) -> Bool { + if lhs.eventId != rhs.eventId { + return lhs.eventId < rhs.eventId + } else { + return lhs.contentIndex.rawValue < rhs.contentIndex.rawValue + } + } + + var hashValue: Int { + return self.eventId.hashValue &+ 31 &* self.contentIndex.rawValue.hashValue + } +} + +private func eventNeedsHeader(_ event: AdminLogEvent) -> Bool { + switch event.action { + case .changeAbout, .changeUsername, .editMessage, .deleteMessage: + return true + case let .updatePinned(message): + if message != nil { + return true + } else { + return false + } + default: + return false + } +} + +private func appendAttributedText(text: (String, [(Int, NSRange)]), generateEntities: (Int) -> [MessageTextEntityType], to string: inout String, entities: inout [MessageTextEntity]) { + for (index, range) in text.1 { + for type in generateEntities(index) { + entities.append(MessageTextEntity(range: (string.count + range.lowerBound) ..< (string.count + range.upperBound), type: type)) + } + } + string.append(text.0) +} + +private func filterOriginalMessageFlags(_ message: Message) -> Message { + return message.withUpdatedFlags([.Incoming]) +} + +private func filterMessageChannelPeer(_ peer: Peer) -> Peer { + if let peer = peer as? TelegramChannel { + return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, peerGroupId: peer.peerGroupId) + } + return peer +} + +struct ChatRecentActionsEntry: Comparable, Identifiable { + let id: ChatRecentActionsEntryId + let presentationData: ChatPresentationData + let entry: ChannelAdminEventLogEntry + + static func ==(lhs: ChatRecentActionsEntry, rhs: ChatRecentActionsEntry) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.entry != rhs.entry { + return false + } + return true + } + + static func <(lhs: ChatRecentActionsEntry, rhs: ChatRecentActionsEntry) -> Bool { + if lhs.entry.event.date != rhs.entry.event.date { + return lhs.entry.event.date < rhs.entry.event.date + } else { + return lhs.id < rhs.id + } + } + + var stableId: ChatRecentActionsEntryId { + return self.id + } + + func item(account: Account, peer: Peer, controllerInteraction: ChatControllerInteraction) -> ListViewItem { + switch self.entry.event.action { + case let .changeTitle(_, new): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + + let action = TelegramMediaActionType.titleUpdated(title: new) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .changeAbout(prev, new): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + + switch self.id.contentIndex { + case .header: + var text: String = "" + var entities: [MessageTextEntity] = [] + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedChannelAbout(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedGroupAbout(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case .content: + let peers = SimpleDictionary() + let attributes: [MessageAttribute] = [] + let prevMessage = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + } + case let .changeUsername(prev, new): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + switch self.id.contentIndex { + case .header: + var text: String = "" + var entities: [MessageTextEntity] = [] + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedChannelUsername(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedGroupUsername(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case .content: + var previousAttributes: [MessageAttribute] = [] + var attributes: [MessageAttribute] = [] + + let prevText = "https://t.me/\(prev)" + previousAttributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< prevText.count, type: .Url)])) + + let text: String + if !new.isEmpty { + text = "https://t.me/\(new)" + attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Url)])) + } else { + text = self.presentationData.strings.Channel_AdminLog_EmptyMessageText + attributes.append(TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< text.count, type: .Italic)])) + } + + let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + } + case let .changePhoto(_, new): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + + var photo: TelegramMediaImage? + if !new.isEmpty { + photo = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new, reference: nil) + } + + let action = TelegramMediaActionType.photoUpdated(image: photo) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .toggleInvites(value): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + if value { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleInvitesOn(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleInvitesOff(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .toggleSignatures(value): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + if value { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOn(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOff(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .updatePinned(message): + switch self.id.contentIndex { + case .header: + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessagePinned(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case .content: + if let message = message { + var peers = SimpleDictionary() + var attributes: [MessageAttribute] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + attributes.append(attribute) + } + } + for attribute in attributes { + for peerId in attribute.associatedPeerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + } else { + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageUnpinned(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + } + } + case let .editMessage(prev, message): + switch self.id.contentIndex { + case .header: + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageEdited(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case .content: + var peers = SimpleDictionary() + var attributes: [MessageAttribute] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + attributes.append(attribute) + } + } + for attribute in attributes { + for peerId in attribute.associatedPeerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + } + case let .deleteMessage(message): + switch self.id.contentIndex { + case .header: + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + + var text: String = "" + var entities: [MessageTextEntity] = [] + + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageDeleted(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case .content: + var peers = SimpleDictionary() + var attributes: [MessageAttribute] = [] + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + attributes.append(attribute) + } + } + for attribute in attributes { + for peerId in attribute.associatedPeerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + } + case .participantJoin, .participantLeave: + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + + let action: TelegramMediaActionType + if case .participantJoin = self.entry.event.action { + action = TelegramMediaActionType.addedMembers(peerIds: [self.entry.event.peerId]) + } else { + action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .participantInvite(participant): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = peer + for (_, peer) in participant.peers { + peers[peer.id] = peer + } + + let action: TelegramMediaActionType + action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .participantToggleBan(prev, new): + var peers = SimpleDictionary() + var attributes: [MessageAttribute] = [] + + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = filterMessageChannelPeer(peer) + + var text: String = "" + var entities: [MessageTextEntity] = [] + + if case let .member(_, _, _, prevBanInfo) = prev.participant { + if case let .member(_, _, _, newBanInfo) = new.participant { + let newFlags = newBanInfo?.rights.flags ?? [] + + if (prevBanInfo == nil || !prevBanInfo!.rights.flags.contains(.banReadMessages)) && newFlags.contains(.banReadMessages) { + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageKickedName(new.peer.displayTitle) : self.presentationData.strings.Channel_AdminLog_MessageKickedNameUsername(new.peer.displayTitle, "@" + new.peer.addressName!), generateEntities: { index in + var result: [MessageTextEntityType] = [] + if index == 0 { + result.append(.TextMention(peerId: new.peer.id)) + } else if index == 1 { + result.append(.Mention) + } + return result + }, to: &text, entities: &entities) + text += "\n" + } else { + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRestrictedName(new.peer.displayTitle) : self.presentationData.strings.Channel_AdminLog_MessageRestrictedNameUsername(new.peer.displayTitle, "@" + new.peer.addressName!), generateEntities: { index in + var result: [MessageTextEntityType] = [] + if index == 0 { + result.append(.TextMention(peerId: new.peer.id)) + } else if index == 1 { + result.append(.Mention) + } + return result + }, to: &text, entities: &entities) + text += "\n" + + if let newBanInfo = newBanInfo, newBanInfo.rights.untilDate != 0 && newBanInfo.rights.untilDate != Int32.max { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: self.presentationData.strings.languageCode) + formatter.dateFormat = "E, d MMM HH:mm" + let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(newBanInfo.rights.untilDate))) + + if prevBanInfo?.rights.flags != newBanInfo.rights.flags { + text += self.presentationData.strings.Channel_AdminLog_MessageRestrictedUntil(dateString).0 + } else { + text += self.presentationData.strings.Channel_AdminLog_MessageRestrictedNewSetting(dateString).0 + } + text += "\n" + } else { + if prevBanInfo?.rights.flags != newBanInfo?.rights.flags { + text += self.presentationData.strings.Channel_AdminLog_MessageRestrictedForever + } else { + text += self.presentationData.strings.Channel_AdminLog_MessageRestrictedNewSetting(self.presentationData.strings.Channel_AdminLog_MessageRestrictedForever).0 + } + text += "\n" + } + + let prevFlags = prevBanInfo?.rights.flags ?? [] + + let order: [(TelegramChannelBannedRightsFlags, String)] = [ + (.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages), + (.banSendMessages, self.presentationData.strings.Channel_AdminLog_BanSendMessages), + (.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia), + (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickers), + (.banSendGifs, self.presentationData.strings.Channel_AdminLog_BanSendGifs), + (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), + ] + + for (flag, string) in order { + if prevFlags.contains(flag) != newFlags.contains(flag) { + text += "\n" + if prevFlags.contains(flag) { + text += "+" + } else { + text += "-" + } + text += string + } + } + } + } + } + + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + + for attribute in attributes { + for peerId in attribute.associatedPeerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .participantToggleAdmin(prev, new): + var peers = SimpleDictionary() + var attributes: [MessageAttribute] = [] + + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + peers[peer.id] = filterMessageChannelPeer(peer) + + var text: String = "" + var entities: [MessageTextEntity] = [] + + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(new.peer.displayTitle) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(new.peer.displayTitle, "@" + new.peer.addressName!), generateEntities: { index in + var result: [MessageTextEntityType] = [] + if index == 0 { + result.append(.TextMention(peerId: new.peer.id)) + } else if index == 1 { + result.append(.Mention) + } + return result + }, to: &text, entities: &entities) + text += "\n" + + if case let .member(_, _, prevAdminRights, _) = prev.participant { + if case let .member(_, _, newAdminRights, _) = new.participant { + let prevFlags = prevAdminRights?.rights.flags ?? [] + let newFlags = newAdminRights?.rights.flags ?? [] + + let order: [(TelegramChannelAdminRightsFlags, String)] = [ + (.canChangeInfo, self.presentationData.strings.Channel_AdminLog_CanChangeInfo), + (.canPostMessages, self.presentationData.strings.Channel_AdminLog_CanSendMessages), + (.canDeleteMessages, self.presentationData.strings.Channel_AdminLog_CanDeleteMessages), + (.canBanUsers, self.presentationData.strings.Channel_AdminLog_CanBanUsers), + (.canEditMessages, self.presentationData.strings.Channel_AdminLog_CanEditMessages), + (.canInviteUsers, self.presentationData.strings.Channel_AdminLog_CanChangeInviteLink), + (.canChangeInviteLink, self.presentationData.strings.Channel_AdminLog_CanInviteUsers), + (.canPinMessages, self.presentationData.strings.Channel_AdminLog_CanPinMessages), + (.canAddAdmins, self.presentationData.strings.Channel_AdminLog_CanAddAdmins) + ] + + for (flag, string) in order { + if prevFlags.contains(flag) != newFlags.contains(flag) { + text += "\n" + if !prevFlags.contains(flag) { + text += "+" + } else { + text += "-" + } + text += string + } + } + } + } + + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + + for attribute in attributes { + for peerId in attribute.associatedPeerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .changeStickerPack(_, new): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + + if new != nil { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedGroupStickerPack(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageRemovedGroupStickerPack(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + case let .togglePreHistoryHidden(value): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + if value { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageGroupPreHistoryVisible(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageGroupPreHistoryHidden(author?.displayTitle ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none)) + } + } +} + +func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData) -> [ChatRecentActionsEntry] { + var result: [ChatRecentActionsEntry] = [] + for entry in entries.reversed() { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry)) + if eventNeedsHeader(entry.event) { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .header), presentationData: presentationData, entry: entry)) + } + } + + assert(result == result.sorted().reversed()) + return result +} + +struct ChatRecentActionsHistoryTransition { + let filteredEntries: [ChatRecentActionsEntry] + let type: ChannelAdminEventLogUpdateType + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let canLoadEarlier: Bool + let displayingResults: Bool +} + +func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentActionsEntry], to toEntries: [ChatRecentActionsEntry], type: ChannelAdminEventLogUpdateType, canLoadEarlier: Bool, displayingResults: Bool, account: Account, peer: Peer, controllerInteraction: ChatControllerInteraction) -> ChatRecentActionsHistoryTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peer: peer, controllerInteraction: controllerInteraction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peer: peer, controllerInteraction: controllerInteraction), directionHint: nil) } + + return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults) +} diff --git a/TelegramUI/ChatRecentActionsInteraction.swift b/TelegramUI/ChatRecentActionsInteraction.swift new file mode 100644 index 0000000000..ce7ff3ee30 --- /dev/null +++ b/TelegramUI/ChatRecentActionsInteraction.swift @@ -0,0 +1,9 @@ +import Foundation + +final class ChatRecentActionsInteraction { + let displayInfoAlert: () -> Void + + init(displayInfoAlert: @escaping () -> Void) { + self.displayInfoAlert = displayInfoAlert + } +} diff --git a/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift b/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift new file mode 100644 index 0000000000..3768e2f18e --- /dev/null +++ b/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift @@ -0,0 +1,64 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +private let searchBarFont = Font.regular(14.0) + +final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + private let searchBar: SearchBarNode + + private var queryUpdated: ((String) -> Void)? + + init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: theme, strings: strings) + let placeholderText = strings.Common_Search + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query in + self?.queryUpdated?(query) + } + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let searchBarFrame = CGRect(origin: CGPoint(), size: size) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} diff --git a/TelegramUI/ChatRecentActionsTitleView.swift b/TelegramUI/ChatRecentActionsTitleView.swift new file mode 100644 index 0000000000..adb83f6ca4 --- /dev/null +++ b/TelegramUI/ChatRecentActionsTitleView.swift @@ -0,0 +1,99 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private func generateArrowImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 8.0, height: 5.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.beginPath() + context.move(to: CGPoint()) + context.addLine(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height)) + context.closePath() + context.fillPath() + }) +} + +final class ChatRecentActionsTitleView: UIView { + private let button: HighlightTrackingButtonNode + private let titleNode: TextNode + private let arrowNode: ASImageNode + + private var color: UIColor + + var pressed: (() -> Void)? + + var title: String = "" { + didSet { + if self.title != oldValue { + self.setNeedsLayout() + } + } + } + + init(color: UIColor) { + self.color = color + + self.button = HighlightTrackingButtonNode() + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + + self.arrowNode = ASImageNode() + self.arrowNode.isLayerBacked = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.image = generateArrowImage(color: color) + + super.init(frame: CGRect()) + + self.addSubnode(self.titleNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.arrowNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.arrowNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + + self.button.frame = CGRect(origin: CGPoint(), size: size) + + let makeLayout = TextNode.asyncLayout(self.titleNode) + let (titleLayout, titleApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: self.color), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + self.titleNode.frame = titleFrame + let _ = titleApply() + + if let image = self.arrowNode.image { + self.arrowNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 9.0), size: image.size) + } + } + + @objc func buttonPressed() { + self.pressed?() + } +} diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 1a7ae19cfe..0aaaa63754 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -115,10 +115,10 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { var resultCount: Int? var resultsText: NSAttributedString? if let results = interfaceState.search?.resultsState { - resultCount = results.messageIds.count - if let currentId = results.currentId, let index = results.messageIds.index(of: currentId) { + resultCount = results.messageIndices.count + if let currentId = results.currentId, let index = results.messageIndices.index(where: { $0.id == currentId }) { resultIndex = index - resultsText = NSAttributedString(string: "\(index + 1) \(interfaceState.strings.Common_of) \(results.messageIds.count)", font: labelFont, textColor: interfaceState.theme.chat.inputPanel.primaryTextColor) + resultsText = NSAttributedString(string: "\(index + 1) \(interfaceState.strings.Common_of) \(results.messageIndices.count)", font: labelFont, textColor: interfaceState.theme.chat.inputPanel.primaryTextColor) } else { resultsText = NSAttributedString(string: interfaceState.strings.Conversation_SearchNoResults, font: labelFont, textColor: interfaceState.theme.chat.inputPanel.primaryTextColor) } diff --git a/TelegramUI/ChatSearchNavigationContentNode.swift b/TelegramUI/ChatSearchNavigationContentNode.swift index eb7763d7b6..9405173405 100644 --- a/TelegramUI/ChatSearchNavigationContentNode.swift +++ b/TelegramUI/ChatSearchNavigationContentNode.swift @@ -9,17 +9,26 @@ private let searchBarFont = Font.regular(14.0) final class ChatSearchNavigationContentNode: NavigationBarContentNode { private let theme: PresentationTheme private let strings: PresentationStrings + private let chatLocation: ChatLocation private let searchBar: SearchBarNode private let interaction: ChatPanelInterfaceInteraction - init(theme: PresentationTheme, strings: PresentationStrings, interaction: ChatPanelInterfaceInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction) { self.theme = theme self.strings = strings + self.chatLocation = chatLocation self.interaction = interaction self.searchBar = SearchBarNode(theme: theme, strings: strings) - self.searchBar.placeholderString = NSAttributedString(string: strings.Conversation_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + let placeholderText: String + switch chatLocation { + case .peer: + placeholderText = strings.Conversation_SearchPlaceholder + case .group: + placeholderText = "Search this feed" + } + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) super.init() @@ -62,7 +71,14 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { switch search.domain { case .everything: self.searchBar.prefixString = nil - self.searchBar.placeholderString = NSAttributedString(string: strings.Conversation_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + let placeholderText: String + switch self.chatLocation { + case .peer: + placeholderText = self.strings.Conversation_SearchPlaceholder + case .group: + placeholderText = "Search this feed" + } + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) case .members: self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputTextColor) self.searchBar.placeholderString = nil diff --git a/TelegramUI/ChatTextInputAttributes.swift b/TelegramUI/ChatTextInputAttributes.swift new file mode 100644 index 0000000000..58d595b3b9 --- /dev/null +++ b/TelegramUI/ChatTextInputAttributes.swift @@ -0,0 +1,378 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox + +private let alphanumericCharacters = CharacterSet.alphanumerics + +struct ChatTextInputAttributes { + static let bold = NSAttributedStringKey(rawValue: "Attribute__Bold") + static let italic = NSAttributedStringKey(rawValue: "Attribute__Italic") + static let monospace = NSAttributedStringKey(rawValue: "Attribute__Monospace") + static let textMention = NSAttributedStringKey(rawValue: "Attribute__TextMention") +} + +func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { + let result = NSMutableAttributedString(string: text.string) + let fullRange = NSRange(location: 0, length: result.length) + + text.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.textMention { + result.addAttribute(key, value: value, range: range) + } else if key == ChatTextInputAttributes.bold || key == ChatTextInputAttributes.italic || key == ChatTextInputAttributes.monospace { + result.addAttribute(key, value: value, range: range) + } + } + }) + return result +} + +private struct FontAttributes: OptionSet { + var rawValue: Int32 = 0 + + static let bold = FontAttributes(rawValue: 1 << 0) + static let italic = FontAttributes(rawValue: 1 << 1) + static let monospace = FontAttributes(rawValue: 1 << 2) +} + +func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor) -> NSAttributedString { + let result = NSMutableAttributedString(string: stateText.string) + let fullRange = NSRange(location: 0, length: result.length) + + result.addAttribute(NSAttributedStringKey.font, value: Font.regular(fontSize), range: fullRange) + result.addAttribute(NSAttributedStringKey.foregroundColor, value: textColor, range: fullRange) + + stateText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in + var fontAttributes: FontAttributes = [] + + for (key, value) in attributes { + if key == ChatTextInputAttributes.textMention { + result.addAttribute(key, value: value, range: range) + result.addAttribute(NSAttributedStringKey.foregroundColor, value: accentTextColor, range: range) + } else if key == ChatTextInputAttributes.bold { + result.addAttribute(key, value: value, range: range) + fontAttributes.insert(.bold) + } else if key == ChatTextInputAttributes.italic { + result.addAttribute(key, value: value, range: range) + fontAttributes.insert(.italic) + } else if key == ChatTextInputAttributes.monospace { + result.addAttribute(key, value: value, range: range) + fontAttributes.insert(.monospace) + } + } + + if !fontAttributes.isEmpty { + var font: UIFont? + if fontAttributes == [.bold, .italic, .monospace] { + + } else if fontAttributes == [.bold, .italic] { + font = Font.semiboldItalic(fontSize) + } else if fontAttributes == [.bold] { + font = Font.semibold(fontSize) + } else if fontAttributes == [.italic] { + font = Font.italic(fontSize) + } else if fontAttributes == [.monospace] { + font = Font.monospace(fontSize) + } + + if let font = font { + result.addAttribute(NSAttributedStringKey.font, value: font, range: range) + } + } + }) + return result +} + +private func textMentionRangesEqual(_ lhs: [(NSRange, ChatTextInputTextMentionAttribute)], _ rhs: [(NSRange, ChatTextInputTextMentionAttribute)]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if lhs[i].0 != rhs[i].0 || lhs[i].1.peerId != rhs[i].1.peerId { + return false + } + } + return true +} + +final class ChatTextInputTextMentionAttribute: NSObject { + let peerId: PeerId + + init(peerId: PeerId) { + self.peerId = peerId + + super.init() + } + + override func isEqual(_ object: Any?) -> Bool { + if let other = object as? ChatTextInputTextMentionAttribute { + return self.peerId == other.peerId + } else { + return false + } + } +} + +func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { + guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { + return + } + + let text: NSString = initialAttributedText.string as NSString + let fullRange = NSRange(location: 0, length: initialAttributedText.length) + + let attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) + + var textMentionRanges: [(NSRange, ChatTextInputTextMentionAttribute)] = [] + initialAttributedText.enumerateAttribute(ChatTextInputAttributes.textMention, in: fullRange, options: [], using: { value, range, _ in + if let value = value as? ChatTextInputTextMentionAttribute { + textMentionRanges.append((range, value)) + } + }) + textMentionRanges.sort(by: { $0.0.location < $1.0.location }) + let initialTextMentionRanges = textMentionRanges + + for i in 0 ..< textMentionRanges.count { + let range = textMentionRanges[i].0 + + var validLower = range.lowerBound + inner1: for i in range.lowerBound ..< range.upperBound { + if let c = UnicodeScalar(text.character(at: i)) { + if alphanumericCharacters.contains(c) || c == " " as UnicodeScalar { + validLower = i + break inner1 + } + } else { + break inner1 + } + } + var validUpper = range.upperBound + inner2: for i in (validLower ..< range.upperBound).reversed() { + if let c = UnicodeScalar(text.character(at: i)) { + if alphanumericCharacters.contains(c) || c == " " as UnicodeScalar { + validUpper = i + 1 + break inner2 + } + } else { + break inner2 + } + } + + let maxUpper = (i == textMentionRanges.count - 1) ? fullRange.upperBound : textMentionRanges[i + 1].0.lowerBound + inner3: for i in validUpper ..< maxUpper { + if let c = UnicodeScalar(text.character(at: i)) { + if alphanumericCharacters.contains(c) { + validUpper = i + 1 + } else { + break inner3 + } + } else { + break inner3 + } + } + + textMentionRanges[i] = (NSRange(location: validLower, length: validUpper - validLower), textMentionRanges[i].1) + } + + textMentionRanges = textMentionRanges.filter({ $0.0.length > 0 }) + + while textMentionRanges.count > 1 { + var hadReductions = false + outer: for i in 0 ..< textMentionRanges.count - 1 { + if textMentionRanges[i].1 === textMentionRanges[i + 1].1 { + var combine = true + inner: for j in textMentionRanges[i].0.upperBound ..< textMentionRanges[i + 1].0.lowerBound { + if let c = UnicodeScalar(text.character(at: j)) { + if alphanumericCharacters.contains(c) || c == " " as UnicodeScalar { + } else { + combine = false + break inner + } + } else { + combine = false + break inner + } + } + if combine { + hadReductions = true + textMentionRanges[i] = (NSRange(location: textMentionRanges[i].0.lowerBound, length: textMentionRanges[i + 1].0.upperBound - textMentionRanges[i].0.lowerBound), textMentionRanges[i].1) + textMentionRanges.remove(at: i + 1) + break outer + } + } + } + if !hadReductions { + break + } + } + + if textMentionRanges.count > 1 { + outer: for i in (1 ..< textMentionRanges.count).reversed() { + for j in 0 ..< i { + if textMentionRanges[j].1 === textMentionRanges[i].1 { + textMentionRanges.remove(at: i) + continue outer + } + } + } + } + + if !textMentionRangesEqual(textMentionRanges, initialTextMentionRanges) { + attributedText.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) + for (range, attribute) in textMentionRanges { + attributedText.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: attribute.peerId), range: range) + } + } + + let resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor) + + if !resultAttributedText.isEqual(to: initialAttributedText) { + textNode.textView.textStorage.removeAttribute(NSAttributedStringKey.font, range: fullRange) + textNode.textView.textStorage.removeAttribute(NSAttributedStringKey.foregroundColor, range: fullRange) + textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) + + textNode.textView.textStorage.addAttribute(NSAttributedStringKey.font, value: Font.regular(baseFontSize), range: fullRange) + textNode.textView.textStorage.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) + + attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in + var fontAttributes: FontAttributes = [] + + for (key, value) in attributes { + if key == ChatTextInputAttributes.textMention { + textNode.textView.textStorage.addAttribute(key, value: value, range: range) + textNode.textView.textStorage.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: range) + } else if key == ChatTextInputAttributes.bold { + textNode.textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.bold) + } else if key == ChatTextInputAttributes.italic { + textNode.textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.italic) + } else if key == ChatTextInputAttributes.monospace { + textNode.textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.monospace) + } + } + + if !fontAttributes.isEmpty { + var font: UIFont? + if fontAttributes == [.bold, .italic, .monospace] { + + } else if fontAttributes == [.bold, .italic] { + font = Font.semiboldItalic(baseFontSize) + } else if fontAttributes == [.bold, .monospace] { + + } else if fontAttributes == [.italic, .monospace] { + + } else if fontAttributes == [.bold] { + font = Font.semibold(baseFontSize) + } else if fontAttributes == [.italic] { + font = Font.italic(baseFontSize) + } else if fontAttributes == [.monospace] { + font = Font.monospace(baseFontSize) + } + + if let font = font { + textNode.textView.textStorage.addAttribute(NSAttributedStringKey.font, value: font, range: range) + } + } + }) + } +} + +func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { + var filteredAttributes: [String: Any] = [ + NSAttributedStringKey.font.rawValue: Font.regular(baseFontSize), + NSAttributedStringKey.foregroundColor.rawValue: theme.chat.inputPanel.primaryTextColor + ] + if let attributedText = textNode.attributedText, attributedText.length != 0 { + let attributes = attributedText.attributes(at: max(0, min(textNode.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil) + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + filteredAttributes[key.rawValue] = value + } else if key == ChatTextInputAttributes.italic { + filteredAttributes[key.rawValue] = value + } else if key == ChatTextInputAttributes.monospace { + filteredAttributes[key.rawValue] = value + } else if key == NSAttributedStringKey.font { + filteredAttributes[key.rawValue] = value + } + } + } + textNode.textView.typingAttributes = filteredAttributes +} + +func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, attribute: NSAttributedStringKey) -> ChatTextInputState { + if !state.selectionRange.isEmpty { + let result = NSMutableAttributedString(attributedString: state.inputText) + result.addAttribute(attribute, value: true as Bool, range: NSRange(location: state.selectionRange.lowerBound, length: state.selectionRange.count)) + return ChatTextInputState(inputText: result, selectionRange: state.selectionRange) + } else { + return state + } +} + +private func trimRangesForChatInputText(_ text: NSAttributedString) -> (Int, Int) { + var lower = 0 + var upper = 0 + + let nsString: NSString = text.string as NSString + + for i in 0 ..< nsString.length { + if let c = UnicodeScalar(nsString.character(at: i)) { + if c == " " as UnicodeScalar || c == "\t" as UnicodeScalar || c == "\n" as UnicodeScalar { + lower += 1 + } else { + break + } + } else { + break + } + } + + if lower != nsString.length { + for i in (lower ..< nsString.length).reversed() { + if let c = UnicodeScalar(nsString.character(at: i)) { + if c == " " as UnicodeScalar || c == "\t" as UnicodeScalar || c == "\n" as UnicodeScalar { + upper += 1 + } else { + break + } + } else { + break + } + } + } + + return (lower, upper) +} + +func trimChatInputText(_ text: NSAttributedString) -> NSAttributedString { + let (lower, upper) = trimRangesForChatInputText(text) + if lower == 0 && upper == 0 { + return text + } + + let result = NSMutableAttributedString(attributedString: text) + if upper != 0 { + result.replaceCharacters(in: NSRange(location: result.length - upper, length: upper), with: "") + } + if lower != 0 { + result.replaceCharacters(in: NSRange(location: 0, length: lower), with: "") + } + return result +} + +func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedString] { + if text.length <= 4000 { + return [text] + } else { + var result: [NSAttributedString] = [] + var offset = 0 + while offset < text.length { + result.append(text.attributedSubstring(from: NSRange(location: offset, length: min(text.length - offset, 4000)))) + offset += 4000 + } + return result + } +} diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift index 01daff7f56..73f5e2e9e2 100644 --- a/TelegramUI/ChatTextInputMediaRecordingButton.swift +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -231,6 +231,7 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto self.updateMode(mode: self.mode, animated: false, force: true) self.delegate = self + self.isExclusiveTouch = false; self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel) } diff --git a/TelegramUI/ChatTextInputMenu.swift b/TelegramUI/ChatTextInputMenu.swift new file mode 100644 index 0000000000..cdf85b5239 --- /dev/null +++ b/TelegramUI/ChatTextInputMenu.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit + +enum ChatTextInputMenuState { + case inactive + case general + case format +} + +final class ChatTextInputMenu { + private(set) var state: ChatTextInputMenuState = .inactive { + didSet { + if self.state != oldValue { + switch self.state { + case .inactive: + UIMenuController.shared.menuItems = [] + case .general: + UIMenuController.shared.menuItems = [] + //UIMenuController.shared.menuItems = [UIMenuItem(title: "Format", action: Selector(("_showTextStyleOptions:")))] + case .format: + UIMenuController.shared.menuItems = [ + UIMenuItem(title: "Bold", action: Selector(("formatAttributesBold:"))), + UIMenuItem(title: "Italic", action: Selector(("formatAttributesItalic:"))), + UIMenuItem(title: "Monospace", action: Selector(("formatAttributesMonospace:"))) + ] + UIMenuController.shared.isMenuVisible = true + UIMenuController.shared.update() + } + + } + } + } + + private var observer: NSObjectProtocol? + + init() { + self.observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIMenuControllerDidHideMenu, object: nil, queue: nil, using: { [weak self] _ in + self?.back() + }) + } + + deinit { + if let observer = self.observer { + NotificationCenter.default.removeObserver(observer) + } + } + + func activate() { + if self.state == .inactive { + self.state = .general + } + } + + func deactivate() { + self.state = .inactive + } + + func format() { + if self.state == .general { + self.state = .format + } + } + + func back() { + if self.state == .format { + self.state = .general + } + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 03552010a3..9988c1b2d0 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -81,6 +81,48 @@ private final class AccessoryItemIconButton: HighlightableButton { } } +private func cauclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState) -> CGFloat { + let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + if baseFontSize.isEqual(to: 17.0) { + return 33.0 + } else if baseFontSize.isEqual(to: 19.0) { + return 35.0 + } else if baseFontSize.isEqual(to: 21.0) { + return 38.0 + } else { + return 33.0 + } +} + +private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? +private func textInputBackgroundImage(backgroundColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { + if let current = currentTextInputBackgroundImage { + if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) { + return current.3 + } + } + + let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setBlendMode(.normal) + context.setStrokeColor(strokeColor.cgColor) + let strokeWidth: CGFloat = 0.5 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) + })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) + if let image = image { + currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image) + return image + } else { + return nil + } +} + class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: TextNode var contextPlaceholderNode: TextNode? @@ -120,14 +162,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var keepSendButtonEnabled = false private var extendedSearchLayout = false + private let inputMenu = ChatTextInputMenu() + private var theme: PresentationTheme? private var strings: PresentationStrings? var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { - let text = textInputNode.attributedText?.string ?? "" let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) - return ChatTextInputState(inputText: text, selectionRange: selectionRange) + return ChatTextInputState(inputText: stateAttributedStringForText(textInputNode.attributedText ?? NSAttributedString()), selectionRange: selectionRange) } else { return ChatTextInputState() } @@ -140,17 +183,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) { - if !state.inputText.isEmpty && self.textInputNode == nil { + if state.inputText.length != 0 && self.textInputNode == nil { self.loadTextInputNode() } if let textInputNode = self.textInputNode { self.updatingInputState = true var textColor: UIColor = .black + var accentTextColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = NSAttributedString(string: state.inputText, font: Font.regular(17.0), textColor: textColor) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.keepSendButtonEnabled = keepSendButtonEnabled @@ -173,8 +220,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } set(value) { if let textInputNode = self.textInputNode { var textColor: UIColor = .black + var baseFontSize: CGFloat = 17.0 if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(17.0), textColor: textColor) self.editableTextNodeDidUpdateText(textInputNode) @@ -293,7 +342,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor tintColor = presentationInterfaceState.theme.list.itemAccentColor - //baseFontSize = presentationInterfaceState.fontSize.baseDisplaySize + baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) switch presentationInterfaceState.theme.chat.inputPanel.keyboardColor { case .light: keyboardAppearance = .default @@ -321,6 +370,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputContainer.addSubnode(textInputNode) self.textInputNode = textInputNode + if let presentationInterfaceState = self.presentationInterfaceState { + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + } + if !self.textInputContainer.bounds.size.width.isZero { let textInputFrame = self.textInputContainer.frame @@ -373,9 +426,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accessoryButtonsWidth += button.buttonWidth } + var textFieldMinHeight: CGFloat = 35.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = cauclulateTextFieldMinHeight(presentationInterfaceState) + } + let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { - let unboundTextFieldHeight = max(33.0, ceil(textInputNode.measure(CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(textInputNode.measure(CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) @@ -383,7 +441,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textFieldHeight = min(updatedMaxHeight, unboundTextFieldHeight) } else { - textFieldHeight = 33.0 + textFieldHeight = textFieldMinHeight } return (accessoryButtonsWidth, textFieldHeight) @@ -409,14 +467,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { let textColor = interfaceState.theme.chat.inputPanel.inputTextColor + let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) if let textInputNode = self.textInputNode { if let text = textInputNode.attributedText?.string { let range = textInputNode.selectedRange - textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: textColor) + textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(baseFontSize), textColor: textColor) textInputNode.selectedRange = range } - textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(17.0), NSAttributedStringKey.foregroundColor.rawValue: textColor] + textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(baseFontSize), NSAttributedStringKey.foregroundColor.rawValue: textColor] } } @@ -438,7 +497,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.micButton.updateTheme(theme: interfaceState.theme) - self.textInputBackgroundView.image = PresentationResourcesChat.chatInputTextFieldBackgroundImage(interfaceState.theme) + let textFieldMinHeight = cauclulateTextFieldMinHeight(interfaceState) + let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight + self.textInputBackgroundView.image = textInputBackgroundImage(backgroundColor: interfaceState.theme.chat.inputPanel.panelBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: []) @@ -470,7 +531,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.currentPlaceholder != placeholder { self.currentPlaceholder = placeholder let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) + let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) let _ = placeholderApply() } @@ -502,8 +564,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - let minimalHeight: CGFloat = 47.0 - let minimalInputHeight: CGFloat = 35.0 + var textFieldMinHeight: CGFloat = 33.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = cauclulateTextFieldMinHeight(presentationInterfaceState) + } + let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight var animatedTransition = true if case .immediate = transition { @@ -734,7 +800,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let searchProgressSize = self.searchLayoutProgressView.bounds.size transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize)) - let textInputFrame = CGRect(x: leftInset + self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - self.textFieldInsets.left - self.textFieldInsets.right, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom) + let textInputFrame = CGRect(x: leftInset + self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom) transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) if let textInputNode = self.textInputNode { @@ -834,8 +900,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - if let _ = self.textInputNode { + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { + let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + let inputTextState = self.inputTextState + self.interfaceInteraction?.updateTextInputState({ _ in return inputTextState }) self.updateTextNodeText(animated: true) } @@ -972,6 +1043,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputState({ _ in return inputTextState }) } + + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { + let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + } } @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { @@ -986,13 +1062,66 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { }) if activateGifInput { self.interfaceInteraction?.updateTextInputState { state in - if state.inputText.isEmpty { - return ChatTextInputState(inputText: "@gif ") + if state.inputText.length == 0 { + return ChatTextInputState(inputText: NSAttributedString(string: "@gif ")) } else { return state } } } + self.inputMenu.activate() + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.inputMenu.deactivate() + } + + func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { + if action == Selector(("_showTextStyleOptions:")) { + if case .general = self.inputMenu.state { + if let textInputNode = self.textInputNode, textInputNode.attributedText == nil || textInputNode.attributedText!.length == 0 { + return ASEditableTextNodeTargetForAction(target: nil) + } + return ASEditableTextNodeTargetForAction(target: self) + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) { + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: self) + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: nil) + } + return nil + } + + @objc func _showTextStyleOptions(_ sender: Any) { + self.inputMenu.format() + } + + @objc func formatAttributesBold(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputState { current in + return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold) + } + } + + @objc func formatAttributesItalic(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputState { current in + return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic) + } + } + + @objc func formatAttributesMonospace(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputState { current in + return chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace) + } } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { @@ -1021,23 +1150,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { return true } - - /*for (NSDictionary *item in pasteBoard.items) { - if (item[(__bridge NSString *)kUTTypeJPEG] != nil) { - [images addObject:item[(__bridge NSString *)kUTTypeJPEG]]; - } else if (item[(__bridge NSString *)kUTTypePNG] != nil) { - [images addObject:item[(__bridge NSString *)kUTTypePNG]]; - } else if (item[(__bridge NSString *)kUTTypeGIF] != nil) { - [images addObject:item[(__bridge NSString *)kUTTypeGIF]]; - } else if (item[(__bridge NSString *)kUTTypeURL] != nil) { - id url = item[(__bridge NSString *)kUTTypeURL]; - if ([url respondsToSelector:@selector(characterAtIndex:)]) { - text = url; - } else if ([url isKindOfClass:[NSURL class]]) { - text = ((NSURL *)url).absoluteString; - } - } - }*/ } @objc func sendButtonPressed() { @@ -1051,19 +1163,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func searchLayoutClearButtonPressed() { if let interfaceInteraction = self.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in - var mentionQueryRange: Range? + var mentionQueryRange: NSRange? inner: for (_, type, queryRange) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.contextRequest] { mentionQueryRange = queryRange break inner } } - if let mentionQueryRange = mentionQueryRange, !mentionQueryRange.isEmpty { - var inputText = textInputState.inputText - inputText.replaceSubrange(mentionQueryRange, with: "") + if let mentionQueryRange = mentionQueryRange, mentionQueryRange.length > 0 { + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) + + let rangeLower = mentionQueryRange.lowerBound + let rangeUpper = mentionQueryRange.upperBound + + inputText.replaceCharacters(in: NSRange(location: rangeLower, length: rangeUpper - rangeLower), with: "") + return ChatTextInputState(inputText: inputText) } else { - return ChatTextInputState(inputText: "") + return ChatTextInputState(inputText: NSAttributedString(string: "")) } } } diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index e6294a3add..6652b18189 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -65,6 +65,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private var theme: PresentationTheme private var strings: PresentationStrings + private var timeFormat: PresentationTimeFormat private let contentContainer: ASDisplayNode private let titleNode: ASTextNode @@ -251,7 +252,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } else if let peer = peerViewMainPeer(peerView), let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) + let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, timeFormat: self.timeFormat, presence: presence, relativeTo: Int32(timestamp)) let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: attributedString) { self.infoNode.attributedText = attributedString @@ -333,10 +334,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) { self.account = account self.theme = theme self.strings = strings + self.timeFormat = timeFormat self.contentContainer = ASDisplayNode() diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index b8f874e325..b24f6cf252 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -71,6 +71,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.stackFromBottom = true self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true super.init(account: account, theme: theme, strings: strings) @@ -101,7 +102,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) } else { interfaceInteraction.updateTextInputState { textInputState in - var commandQueryRange: Range? + var commandQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.command] { commandQueryRange = range @@ -110,21 +111,15 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } if let range = commandQueryRange { - var inputText = textInputState.inputText + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) let replacementText = command.command.text + " " - inputText.replaceSubrange(range, with: replacementText) - guard let lowerBound = range.lowerBound.samePosition(in: inputText.utf16) else { - return textInputState - } - let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: lowerBound) + inputText.replaceCharacters(in: range, with: replacementText) - let replacementLength = replacementText.utf16.distance(from: replacementText.utf16.startIndex, to: replacementText.utf16.endIndex) + let selectionPosition = range.lowerBound + (replacementText as NSString).length - let utfUpperPosition = utfLowerIndex + replacementLength - - return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) + return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) } return textInputState } diff --git a/TelegramUI/CompomentsThemes.swift b/TelegramUI/ComponentsThemes.swift similarity index 68% rename from TelegramUI/CompomentsThemes.swift rename to TelegramUI/ComponentsThemes.swift index 21a6178215..563047140c 100644 --- a/TelegramUI/CompomentsThemes.swift +++ b/TelegramUI/ComponentsThemes.swift @@ -27,3 +27,14 @@ extension ActionSheetController { self.init(theme: ActionSheetControllerTheme(presentationTheme: presentationTheme)) } } + +public extension AlertControllerTheme { + convenience init(presentationTheme: PresentationTheme) { + let actionSheet = presentationTheme.actionSheet + self.init(backgroundColor: actionSheet.opaqueItemBackgroundColor, separatorColor: actionSheet.opaqueItemSeparatorColor, highlightedItemColor: actionSheet.opaqueItemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) + } + + convenience init(authTheme: AuthorizationTheme) { + self.init(backgroundColor: authTheme.backgroundColor, separatorColor: authTheme.separatorColor, highlightedItemColor: authTheme.itemHighlightedBackgroundColor, primaryColor: authTheme.primaryColor, secondaryColor: authTheme.textPlaceholderColor, accentColor: authTheme.accentColor, destructiveColor: authTheme.destructiveColor) + } +} diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index f8afc6ef4c..fea13d7c19 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -129,7 +129,7 @@ public class ComposeController: ViewController { let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } controller.displayNavigationActivity = false - controller.present(standardTextAlertController(title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 99774d09de..758211ce6c 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -92,7 +92,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.openPeer(peer) }) } diff --git a/TelegramUI/ContactMultiselectionController.swift b/TelegramUI/ContactMultiselectionController.swift index 8d4f5a03fb..9a432a110f 100644 --- a/TelegramUI/ContactMultiselectionController.swift +++ b/TelegramUI/ContactMultiselectionController.swift @@ -192,7 +192,7 @@ public class ContactMultiselectionController: ViewController { strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)) if displayCountAlert { - strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.CreateGroup_SoftUserLimitAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.CreateGroup_SoftUserLimitAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index f7eb9e25bd..562c61df19 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -60,10 +60,16 @@ struct ContactsPeerItemEditing: Equatable { } } +enum ContactsPeerItemPeerMode { + case generalSearch + case peer +} + class ContactsPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let account: Account + let peerMode: ContactsPeerItemPeerMode let peer: Peer? let chatPeer: Peer? let status: ContactsPeerItemStatus @@ -80,10 +86,11 @@ class ContactsPeerItem: ListViewItem { let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerMode: ContactsPeerItemPeerMode, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account + self.peerMode = peerMode self.peer = peer self.chatPeer = chatPeer self.status = status @@ -165,6 +172,7 @@ class ContactsPeerItem: ListViewItem { } func selected(listView: ListView) { + listView.clearHighlightAnimated(true) if let peer = self.peer { self.action(peer) } @@ -355,7 +363,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { textColor = item.theme.list.itemPrimaryTextColor } if let user = peer as? TelegramUser { - if peer.id == item.account.peerId { + if peer.id == item.account.peerId, case .generalSearch = item.peerMode { titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() @@ -426,7 +434,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { if let peer = item.peer { var overrideImage: AvatarNodeImageOverride? - if peer.id == item.account.peerId { + if peer.id == item.account.peerId, case .generalSearch = item.peerMode { overrideImage = .savedMessagesIcon } strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index fa5cf45c57..934d4ba643 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -75,7 +75,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { enabled = canSendMessagesToPeer(peer) } - listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] peer in + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) diff --git a/TelegramUI/ConvertToSupergroupController.swift b/TelegramUI/ConvertToSupergroupController.swift index c1d706b1dd..eb94fc9473 100644 --- a/TelegramUI/ConvertToSupergroupController.swift +++ b/TelegramUI/ConvertToSupergroupController.swift @@ -148,7 +148,7 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V var rightNavigationButton: ItemListNavigationButton? if state.isConverting { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ConvertToSupergroup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift index 1d2438bc08..0688f8e577 100644 --- a/TelegramUI/CreateChannelController.swift +++ b/TelegramUI/CreateChannelController.swift @@ -236,9 +236,9 @@ public func createChannelController(account: Account) -> ViewController { let rightNavigationButton: ItemListNavigationButton if state.creating { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { arguments.done() }) } diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index a1c14c4d91..58945151d9 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -245,9 +245,9 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo let rightNavigationButton: ItemListNavigationButton if state.creating { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Compose_Create, style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Compose_Create), style: .bold, enabled: !state.editingName.composedTitle.isEmpty, action: { arguments.done() }) } diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift index 3ca6d7417b..033faeace6 100644 --- a/TelegramUI/DebugAccountsController.swift +++ b/TelegramUI/DebugAccountsController.swift @@ -70,7 +70,7 @@ private enum DebugAccountsControllerEntry: ItemListNodeEntry { func item(_ arguments: DebugAccountsControllerArguments) -> ListViewItem { switch self { case let .record(theme, record, current): - return ItemListCheckboxItem(theme: theme, title: "\(UInt64(bitPattern: record.id.int64))", checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: "\(UInt64(bitPattern: record.id.int64))", style: .left, checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.switchAccount(record.id) }) case let .loginNewAccount(theme): diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index ed946a0514..d041d6511e 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -430,10 +430,11 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName dismissImpl?() })) }, logout: { - let alertController = standardTextAlertController(title: NSLocalizedString("Settings.LogoutConfirmationTitle", comment: ""), text: NSLocalizedString("Settings.LogoutConfirmationText", comment: ""), actions: [ - TextAlertAction(type: .genericAction, title: "Cancel", action: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Settings_LogoutConfirmationTitle, text: presentationData.strings.Settings_LogoutConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), - TextAlertAction(type: .defaultAction, title: "OK", action: { + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { let _ = logoutFromAccount(id: account.id, accountManager: accountManager).start() }) ]) @@ -446,9 +447,9 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName |> map { presentationData, state, view, wallpapers -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let rightNavigationButton: ItemListNavigationButton if state.updatingName != nil || state.updatingBioText { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { arguments.saveEditingState() }) } diff --git a/TelegramUI/FeedGroupingController.swift b/TelegramUI/FeedGroupingController.swift new file mode 100644 index 0000000000..2c5f377781 --- /dev/null +++ b/TelegramUI/FeedGroupingController.swift @@ -0,0 +1,53 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class FeedGroupingController: ViewController { + private var controllerNode: FeedGroupingControllerNode { + return self.displayNode as! FeedGroupingControllerNode + } + + private let account: Account + private let groupId: PeerGroupId + private var presentationData: PresentationData + + init(account: Account, groupId: PeerGroupId) { + self.account = account + self.groupId = groupId + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = "Grouping" + + /*let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)*/ + + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = FeedGroupingControllerNode(account: self.account, groupId: self.groupId, presentationData: self.presentationData, ungroupedAll: { [weak self] in + (self?.navigationController as? NavigationController)?.popToRoot(animated: true) + }) + + self.displayNodeDidLoad() + + self.ready.set(self.controllerNode.readyPromise.get()) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} + diff --git a/TelegramUI/FeedGroupingControllerNode.swift b/TelegramUI/FeedGroupingControllerNode.swift new file mode 100644 index 0000000000..7a5b308cf4 --- /dev/null +++ b/TelegramUI/FeedGroupingControllerNode.swift @@ -0,0 +1,475 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +import SafariServices + +private enum FeedGroupingControllerTransitionType { + case initial + case initialLoad + case generic + case load +} + +private struct FeedGroupingControllerTransition { + let type: FeedGroupingControllerTransitionType + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private final class FeedGroupingControllerOpaqueState { + let entries: [FeedGroupingEntry] + let canLoadEarlier: Bool + + init(entries: [FeedGroupingEntry], canLoadEarlier: Bool) { + self.entries = entries + self.canLoadEarlier = canLoadEarlier + } +} + +private final class FeedGroupingControllerArguments { + let account: Account + let togglePeer: (Peer, Bool) -> Void + let ungroupAll: () -> Void + + init(account: Account, togglePeer: @escaping (Peer, Bool) -> Void, ungroupAll: @escaping () -> Void) { + self.account = account + self.togglePeer = togglePeer + self.ungroupAll = ungroupAll + } +} + +private enum FeedGroupingEntryId: Hashable { + case index(Int) + case peer(PeerId) + + static func ==(lhs: FeedGroupingEntryId, rhs: FeedGroupingEntryId) -> Bool { + switch lhs { + case let .index(value): + if case .index(value) = rhs { + return true + } else { + return false + } + case let .peer(id): + if case .peer(id) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .index(value): + return value.hashValue + case let .peer(id): + return id.hashValue + } + } +} + +private enum FeedGroupingSection: ItemListSectionId { + case peers + case ungroup +} + +private enum FeedGroupingEntry: ItemListNodeEntry { + case groupHeader(PresentationTheme, String) + case peer(PresentationTheme, PresentationStrings, Int, Peer, Bool) + case ungroup(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .groupHeader, .peer: + return FeedGroupingSection.peers.rawValue + case .ungroup: + return FeedGroupingSection.ungroup.rawValue + } + } + + var stableId: FeedGroupingEntryId { + switch self { + case .groupHeader: + return .index(0) + case let .peer(_, _, _, peer, _): + return .peer(peer.id) + case .ungroup: + return .index(1) + } + } + + static func ==(lhs: FeedGroupingEntry, rhs: FeedGroupingEntry) -> Bool { + switch lhs { + case let .groupHeader(lhsTheme, lhsText): + if case let .groupHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peer(lhsTheme, lhsStrings, lhsIndex, lhsPeer, lhsValue): + if case let .peer(rhsTheme, rhsStrings, rhsIndex, rhsPeer, rhsValue) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsIndex != rhsIndex { + return false + } + if !lhsPeer.isEqual(rhsPeer) { + return false + } + if lhsValue != rhsValue { + return false + } + return true + } else { + return false + } + case let .ungroup(lhsTheme, lhsText): + if case let .ungroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: FeedGroupingEntry, rhs: FeedGroupingEntry) -> Bool { + switch lhs { + case .groupHeader: + return true + case let .peer(_, _, index, _, _): + switch rhs { + case .groupHeader: + return false + case let .peer(_, _, rhsIndex, _, _): + return index < rhsIndex + default: + return true + } + case .ungroup: + return false + } + } + + func item(_ arguments: FeedGroupingControllerArguments) -> ListViewItem { + switch self { + case let .groupHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .peer(theme, strings, _, peer, value): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: value, style: .standard), enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in + arguments.togglePeer(peer, value) + }) + case let .ungroup(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.ungroupAll() + }) + } + } +} + +private final class FeedGroupingPeerState { + let peer: Peer + let included: Bool + + init(peer: Peer, included: Bool) { + self.peer = peer + self.included = included + } +} + +private final class FeedGroupingEntriesState { + let entries: [FeedGroupingEntry] + + init(entries: [FeedGroupingEntry]) { + self.entries = entries + } +} + +private final class FeedGroupingState { + let theme: PresentationTheme + let strings: PresentationStrings + let peers: [FeedGroupingPeerState] + + init(theme: PresentationTheme, strings: PresentationStrings, peers: [FeedGroupingPeerState]) { + self.theme = theme + self.strings = strings + self.peers = peers + } + + func withUpdatedPeers(_ peers: [FeedGroupingPeerState]) -> FeedGroupingState { + return FeedGroupingState(theme: self.theme, strings: self.strings, peers: peers) + } +} + +private func entriesStateFromState(_ state: FeedGroupingState) -> FeedGroupingEntriesState { + var entries: [FeedGroupingEntry] = [] + if !state.peers.isEmpty { + entries.append(.groupHeader(state.theme, "GROUP CHANNELS")) + var index = 0 + for peer in state.peers { + entries.append(.peer(state.theme, state.strings, index, peer.peer, peer.included)) + index += 1 + } + entries.append(.ungroup(state.theme, "Ungroup All Channels")) + } + return FeedGroupingEntriesState(entries: entries) +} + +private func preparedItemListNodeEntryTransition(from fromEntries: [FeedGroupingEntry], to toEntries: [FeedGroupingEntry], arguments: FeedGroupingControllerArguments, type: FeedGroupingControllerTransitionType) -> FeedGroupingControllerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + + return FeedGroupingControllerTransition(type: type, deletions: deletions, insertions: insertions, updates: updates) +} + +final class FeedGroupingControllerNode: ViewControllerTracingNode { + private let account: Account + private let groupId: PeerGroupId + private var presentationData: PresentationData + private let ungroupedAll: () -> Void + + let readyPromise = ValuePromise() + private var ready: Bool = false { + didSet { + if self.ready && !oldValue { + self.readyPromise.set(self.ready) + } + } + } + + private var presentationDataDisposable: Disposable? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let listNode: ListView + private let blockingOverlay: ASDisplayNode + + private var _stateValue: FeedGroupingState + private let statePromise = Promise() + + private var _entriesStateValue = FeedGroupingEntriesState(entries: []) + private let entriesStatePromise = Promise() + + private var enqueuedTransitions: [FeedGroupingControllerTransition] = [] + + private let peersDisposable = MetaDisposable() + + private var transitionDisposable: Disposable? + + init(account: Account, groupId: PeerGroupId, presentationData: PresentationData, ungroupedAll: @escaping () -> Void) { + self.account = account + self.groupId = groupId + self.presentationData = presentationData + self.ungroupedAll = ungroupedAll + + self.listNode = ListView() + self.listNode.isHidden = true + + self.blockingOverlay = ASDisplayNode() + self.blockingOverlay.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self._stateValue = FeedGroupingState(theme: self.presentationData.theme, strings: self.presentationData.strings, peers: []) + self.statePromise.set(.single(self._stateValue)) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.addSubnode(self.listNode) + self.addSubnode(self.blockingOverlay) + + self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in + if let strongSelf = self { + /*if let state = (opaqueTransactionState as? ChatRecentActionsListOpaqueState), state.canLoadEarlier { + if let visible = displayedRange.visibleRange { + let indexRange = (state.entries.count - 1 - visible.lastIndex, state.entries.count - 1 - visible.firstIndex) + if indexRange.0 < 5 { + strongSelf.context.loadMoreEntries() + } + } + }*/ + } + } + + let previousState = Atomic(value: nil) + + let arguments = FeedGroupingControllerArguments(account: account, togglePeer: { [weak self] peer, value in + if let strongSelf = self { + strongSelf.updateState({ current in + var peers = current.peers + var index = 0 + for listPeer in peers { + if listPeer.peer.id == peer.id { + peers[index] = FeedGroupingPeerState(peer: listPeer.peer, included: value) + break + } + index += 1 + } + return current.withUpdatedPeers(peers) + }) + let _ = updatePeerGroupIdInteractively(postbox: strongSelf.account.postbox, peerId: peer.id, groupId: value ? strongSelf.groupId : nil).start() + } + }, ungroupAll: { [weak self] in + if let strongSelf = self { + + let _ = (clearPeerGroupInteractively(postbox: strongSelf.account.postbox, groupId: strongSelf.groupId) + |> deliverOnMainQueue).start(completed: { + self?.ungroupedAll() + }) + } + }) + + self.transitionDisposable = (self.entriesStatePromise.get() + |> mapToQueue { state -> Signal in + let previous = previousState.swap(state) + let type: FeedGroupingControllerTransitionType + if let previous = previous { + if previous.entries.isEmpty { + type = .initialLoad + } else { + type = .generic + } + } else { + type = .initial + } + return .single(preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, arguments: arguments, type: type)) + } + |> deliverOnMainQueue).start(next: { [weak self] transition in + return self?.enqueueTransition(transition) + }) + + self.updateState({ state in + return state + }) + + self.peersDisposable.set((availableGroupFeedPeers(postbox: self.account.postbox, network: self.account.network, groupId: groupId) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.updateState({ state in + return state.withUpdatedPeers(result.map { peer, value in + return FeedGroupingPeerState(peer: peer, included: value) + }) + }) + } + })) + } + + deinit { + self.transitionDisposable?.dispose() + } + + private func updateState(_ f: (FeedGroupingState) -> FeedGroupingState) { + let updatedState = f(self._stateValue) + self._stateValue = updatedState + self._entriesStateValue = entriesStateFromState(updatedState) + self.entriesStatePromise.set(.single(self._entriesStateValue)) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.containerLayout == nil + + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + transition.updateBounds(node: self.listNode, bounds: CGRect(origin: CGPoint(), size: layout.size)) + transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let listInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.right, bottom: insets.bottom, right: layout.safeInsets.left) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if isFirstLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(_ transition: FeedGroupingControllerTransition) { + self.enqueuedTransitions.append(transition) + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + while true { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + switch transition.type { + case .initial: + options.insert(.LowLatency) + case .generic: + options.insert(.AnimateInsertion) + case .load, .initialLoad: + break + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.ready = true + if transition.type == .initialLoad { + strongSelf.listNode.isHidden = false + strongSelf.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + /*if displayingResults != !strongSelf.listNode.isHidden { + strongSelf.listNode.isHidden = !displayingResults + strongSelf.backgroundColor = displayingResults ? strongSelf.presentationData.theme.list.plainBackgroundColor : nil + + strongSelf.emptyNode.isHidden = displayingResults + if !displayingResults { + var text: String = "" + if let query = strongSelf.filter.query { + text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterQueryText(query).0 + } else { + text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterText + } + strongSelf.emptyNode.setup(title: strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterTitle, text: text) + } + //strongSelf.isLoading = isEmpty && !displayingResults + }*/ + } + }) + } else { + break + } + } + } +} + diff --git a/TelegramUI/FetchPhotoLibraryImageResource.swift b/TelegramUI/FetchPhotoLibraryImageResource.swift index 624a2e50ca..04ae4d7696 100644 --- a/TelegramUI/FetchPhotoLibraryImageResource.swift +++ b/TelegramUI/FetchPhotoLibraryImageResource.swift @@ -3,10 +3,15 @@ import Photos import Postbox import SwiftSignalKit +private final class RequestId { + var id: PHImageRequestID? + var invalidated: Bool = false +} + func fetchPhotoLibraryResource(localIdentifier: String) -> Signal { return Signal { subscriber in let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) - var requestId: PHImageRequestID? + let requestId = Atomic(value: RequestId()) if fetchResult.count != 0 { let asset = fetchResult.object(at: 0) let option = PHImageRequestOptions() @@ -20,9 +25,15 @@ func fetchPhotoLibraryResource(localIdentifier: String) -> Signal Void in + + let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in Queue.concurrentDefaultQueue().async { - requestId = nil + requestId.with { current -> Void in + if !current.invalidated { + current.id = nil + current.invalidated = true + } + } if let image = image { if let info = info, let degraded = info[PHImageResultIsDegradedKey], (degraded as AnyObject).boolValue!{ if !madeProgress.swap(true) { @@ -53,13 +64,28 @@ func fetchPhotoLibraryResource(localIdentifier: String) -> Signal Void in + if !current.invalidated { + current.id = requestIdValue + } + } } else { subscriber.putNext(.reset) } return ActionDisposable { - if let requestId = requestId { - PHImageManager.default().cancelImageRequest(requestId) + let requestIdValue = requestId.with { current -> PHImageRequestID? in + if !current.invalidated { + let value = current.id + current.id = nil + current.invalidated = true + return value + } else { + return nil + } + } + if let requestIdValue = requestIdValue { + PHImageManager.default().cancelImageRequest(requestIdValue) } } } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 99c20da50b..6c21db4b5f 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -139,6 +139,11 @@ private enum GalleryMessageHistoryView { } } +enum GalleryControllerItemSource { + case peerMessagesAtId(MessageId) + case standaloneMessage(Message) +} + class GalleryController: ViewController { static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) @@ -179,7 +184,7 @@ class GalleryController: ViewController { private var hiddenMediaManagerIndex: Int? - init(account: Account, messageId: MessageId, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { + init(account: Account, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { self.account = account self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController @@ -194,21 +199,32 @@ class GalleryController: ViewController { self.statusBar.statusBarStyle = .White - let message = account.postbox.messageAtId(messageId) + let message: Signal + switch source { + case let .peerMessagesAtId(messageId): + message = account.postbox.messageAtId(messageId) + case let .standaloneMessage(m): + message = .single(m) + } let messageView = message |> filter({ $0 != nil }) |> mapToSignal { message -> Signal in - if !streamSingleVideo, let tags = tagsForMessage(message!) { - let view = account.postbox.aroundMessageHistoryViewForLocation(.peer(messageId.peerId), index: .message(MessageIndex(message!)), anchorIndex: .message(MessageIndex(message!)), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) - - return view - |> mapToSignal { (view, _, _) -> Signal in - let mapped = GalleryMessageHistoryView.view(view) - return .single(mapped) + switch source { + case .peerMessagesAtId: + if !streamSingleVideo, let tags = tagsForMessage(message!) { + let view = account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), index: .message(MessageIndex(message!)), anchorIndex: .message(MessageIndex(message!)), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) + + return view + |> mapToSignal { (view, _, _) -> Signal in + let mapped = GalleryMessageHistoryView.view(view) + return .single(mapped) + } + } else { + return .single(GalleryMessageHistoryView.single(MessageHistoryEntry.MessageEntry(message!, false, nil, nil))) } - } else { - return .single(GalleryMessageHistoryView.single(MessageHistoryEntry.MessageEntry(message!, false, nil, nil))) + case .standaloneMessage: + return .single(GalleryMessageHistoryView.single(MessageHistoryEntry.MessageEntry(message!, false, nil, nil))) } } |> take(1) @@ -227,9 +243,19 @@ class GalleryController: ViewController { var centralEntryStableId: UInt32? loop: for i in 0 ..< entries.count { switch entries[i] { - case let .MessageEntry(message, _, _, _) where message.id == messageId: - centralEntryStableId = message.stableId - break loop + case let .MessageEntry(message, _, _, _): + switch source { + case let .peerMessagesAtId(messageId): + if message.id == messageId { + centralEntryStableId = message.stableId + break loop + } + case let .standaloneMessage(m): + if message.id == m.id { + centralEntryStableId = message.stableId + break loop + } + } default: break } @@ -258,6 +284,7 @@ class GalleryController: ViewController { strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) if strongSelf.temporaryDoNotWaitForReady { + strongSelf.didSetReady = true strongSelf._ready.set(.single(true)) } else { let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in @@ -458,6 +485,18 @@ class GalleryController: ViewController { } } } + + if !self.entries.isEmpty && !self.didSetReady { + if self.temporaryDoNotWaitForReady { + self.didSetReady = true + self._ready.set(.single(true)) + } else { + let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in + self?.didSetReady = true + } + self._ready.set(ready |> map { true }) + } + } } override func viewDidAppear(_ animated: Bool) { diff --git a/TelegramUI/GenerateTextEntities.swift b/TelegramUI/GenerateTextEntities.swift index b2e5f583fd..8737378b37 100644 --- a/TelegramUI/GenerateTextEntities.swift +++ b/TelegramUI/GenerateTextEntities.swift @@ -77,8 +77,26 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, } } -func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes) -> [MessageTextEntity] { +func generateChatInputTextEntities(_ text: NSAttributedString) -> [MessageTextEntity] { var entities: [MessageTextEntity] = [] + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in + for (key, value) in attributes { + if key == ChatTextInputAttributes.bold { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold)) + } else if key == ChatTextInputAttributes.italic { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Italic)) + } else if key == ChatTextInputAttributes.monospace { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Pre)) + } else if key == ChatTextInputAttributes.textMention, let value = value as? ChatTextInputTextMentionAttribute { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: value.peerId))) + } + } + }) + return entities +} + +func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes, currentEntities: [MessageTextEntity] = []) -> [MessageTextEntity] { + var entities: [MessageTextEntity] = currentEntities let utf16 = text.utf16 diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index 835f3951c6..2d2361d280 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -109,7 +109,7 @@ final class GridMessageItem: GridItem { fileprivate let theme: PresentationTheme private let strings: PresentationStrings private let account: Account - private let message: Message + fileprivate let message: Message private let controllerInteraction: ChatControllerInteraction let section: GridSection? @@ -323,7 +323,7 @@ final class GridMessageItemNode: GridItemNode { } @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - guard let controllerInteraction = self.controllerInteraction, let messageId = self.messageId else { + guard let controllerInteraction = self.controllerInteraction, let message = self.item?.message else { return } @@ -337,19 +337,19 @@ final class GridMessageItemNode: GridItemNode { if let resourceStatus = self.resourceStatus { switch resourceStatus { case .Fetching: - messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file) + messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: file) case .Local: - controllerInteraction.openMessage(messageId) + let _ = controllerInteraction.openMessage(message) case .Remote: - self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start()) + self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: message.id, file: file).start()) } } } else { - controllerInteraction.openMessage(messageId) + let _ = controllerInteraction.openMessage(message) } } case .longTap: - controllerInteraction.openMessageContextMenu(messageId, self, self.bounds) + controllerInteraction.openMessageContextMenu(message, self, self.bounds) default: break } diff --git a/TelegramUI/GroupAdminsController.swift b/TelegramUI/GroupAdminsController.swift index 371a547e88..04c3256307 100644 --- a/TelegramUI/GroupAdminsController.swift +++ b/TelegramUI/GroupAdminsController.swift @@ -56,7 +56,7 @@ private enum GroupAdminsEntryStableId: Hashable { private enum GroupAdminsEntry: ItemListNodeEntry { case allAdmins(PresentationTheme, String, Bool) case allAdminsInfo(PresentationTheme, String) - case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, PeerPresence?, Bool, Bool) + case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, String, Bool, Bool) var section: ItemListSectionId { switch self { @@ -92,8 +92,8 @@ private enum GroupAdminsEntry: ItemListNodeEntry { } else { return false } - case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsPresence, lhsToggled, lhsEnabled): - if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsPresence, rhsToggled, rhsEnabled) = rhs { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsLabel, lhsToggled, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsLabel, rhsToggled, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } @@ -106,11 +106,7 @@ private enum GroupAdminsEntry: ItemListNodeEntry { 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) { + if lhsLabel != rhsLabel { return false } if lhsToggled != rhsToggled { @@ -155,8 +151,8 @@ private enum GroupAdminsEntry: ItemListNodeEntry { }) case let .allAdminsInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .peerItem(_, theme, strings, peer, presence, toggled, enabled): - return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: toggled, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in + case let .peerItem(_, theme, strings, peer, label, toggled, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: toggled, style: .standard), enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in arguments.updatePeerIsAdmin(peer.id, value) }) } @@ -254,6 +250,7 @@ private func groupAdminsControllerEntries(account: Account, presentationData: Pr if let peer = view.peers[participant.peerId] { var isAdmin = false var isEnabled = true + let label = "" if !effectiveAdminsEnabled { isAdmin = true isEnabled = false @@ -276,7 +273,7 @@ private func groupAdminsControllerEntries(account: Account, presentationData: Pr } } } - entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, view.peerPresences[participant.peerId], isAdmin, isEnabled)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, label, isAdmin, isEnabled)) index += 1 } } @@ -364,7 +361,7 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr var rightNavigationButton: ItemListNavigationButton? if !state.updatingAdminValue.isEmpty || state.updatingAllAdminsValue != nil { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index ff5953383e..8d8cfc8f8d 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -507,6 +507,8 @@ private struct GroupInfoState: Equatable { let savingData: Bool + let searchingMembers: Bool + static func ==(lhs: GroupInfoState, rhs: GroupInfoState) -> Bool { if lhs.updatingAvatar != rhs.updatingAvatar { return false @@ -532,39 +534,46 @@ private struct GroupInfoState: Equatable { if lhs.savingData != rhs.savingData { return false } + if lhs.searchingMembers != rhs.searchingMembers { + return false + } return true } func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> GroupInfoState { - return GroupInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedTemporaryParticipants(_ temporaryParticipants: [TemporaryParticipant]) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedSuccessfullyAddedParticipantIds(_ successfullyAddedParticipantIds: Set) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedRemovingParticipantIds(_ removingParticipantIds: Set) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers) } func withUpdatedSavingData(_ savingData: Bool) -> GroupInfoState { - return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData) + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData, searchingMembers: self.searchingMembers) + } + + func withUpdatedSearchingMembers(_ searchingMembers: Bool) -> GroupInfoState { + return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: searchingMembers) } } @@ -682,11 +691,19 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa entries.append(GroupInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsText)) entries.append(GroupInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: peerNotificationSettings.messageSound, default: globalNotificationSettings.effective.groupChats.sound))) - if let adminCount = cachedChannelData.participantsSummary.adminCount { - entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, presentationData.strings.Channel_Info_Management, "\(adminCount)")) + var canViewAdminsAndBanned = false + if let channel = view.peers[view.peerId] as? TelegramChannel { + if let adminRights = channel.adminRights, !adminRights.isEmpty { + canViewAdminsAndBanned = true + } else if channel.flags.contains(.isCreator) { + canViewAdminsAndBanned = true + } } - if let bannedCount = cachedChannelData.participantsSummary.bannedCount { - entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, presentationData.strings.Channel_Info_Banned, "\(bannedCount)")) + + if canViewAdminsAndBanned { + entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, presentationData.strings.Channel_Info_Management, cachedChannelData.participantsSummary.adminCount.flatMap { "\($0)" } ?? "")) + + entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, presentationData.strings.Channel_Info_Banned, cachedChannelData.participantsSummary.bannedCount.flatMap { "\($0)" } ?? "" )) } } } else { @@ -938,8 +955,8 @@ private func valuesRequiringUpdate(state: GroupInfoState, view: PeerView) -> (ti } public func groupInfoController(account: Account, peerId: PeerId) -> ViewController { - let statePromise = ValuePromise(GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false), ignoreRepeated: true) - let stateValue = Atomic(value: GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false)) + let statePromise = ValuePromise(GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false, searchingMembers: false), ignoreRepeated: true) + let stateValue = Atomic(value: GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false, searchingMembers: false)) let updateState: ((GroupInfoState) -> GroupInfoState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -1095,15 +1112,16 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl controller?.dismissAnimated() } let notificationAction: (Int32) -> Void = { muteUntil in - let muteState: PeerMuteState + let muteInterval: Int32? if muteUntil <= 0 { - muteState = .unmuted + muteInterval = nil } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) + muteInterval = Int32.max } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + muteInterval = muteUntil } - changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + + changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start()) } controller.setItemGroups([ ActionSheetItemGroup(items: [ @@ -1198,7 +1216,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl let result = ValuePromise() let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } if let contactsController = contactsController { - let alertController = standardTextAlertController(title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle).0, actions: [ + let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle).0, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { result.set(false) }), @@ -1364,6 +1382,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } let rightNavigationButton: ItemListNavigationButton + var secondaryRightNavigationButton: ItemListNavigationButton? if let editingState = state.editingState { var doneEnabled = true if let editingName = editingState.editingName, editingName.isEmpty { @@ -1376,9 +1395,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } if state.savingData { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: view) @@ -1419,7 +1438,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) } } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { if let peer = peer as? TelegramGroup { updateState { state in return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer), editingDescriptionText: "")) @@ -1434,10 +1453,28 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } } }) + secondaryRightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { + updateState { state in + return state.withUpdatedSearchingMembers(true) + } + }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .blocks) + var searchItem: ItemListControllerSearch? + if state.searchingMembers { + searchItem = GroupInfoSearchItem(account: account, peerId: peerId, cancel: { + updateState { state in + return state.withUpdatedSearchingMembers(false) + } + }, openPeer: { peer in + if let infoController = peerInfoController(account: account, peer: peer) { + arguments.pushController(infoController) + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .blocks, searchItem: searchItem) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/GroupInfoSearchItem.swift b/TelegramUI/GroupInfoSearchItem.swift new file mode 100644 index 0000000000..247722e0b6 --- /dev/null +++ b/TelegramUI/GroupInfoSearchItem.swift @@ -0,0 +1,81 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class GroupInfoSearchItem: ItemListControllerSearch { + let account: Account + let peerId: PeerId + let cancel: () -> Void + let openPeer: (Peer) -> Void + + init(account: Account, peerId: PeerId, cancel: @escaping () -> Void, openPeer: @escaping (Peer) -> Void) { + self.account = account + self.peerId = peerId + self.cancel = cancel + self.openPeer = openPeer + } + + func isEqual(to: ItemListControllerSearch) -> Bool { + if let to = to as? GroupInfoSearchItem { + if self.account !== to.account { + return false + } + if self.peerId != to.peerId { + return false + } + return true + } else { + return false + } + } + + func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode { + if let current = current as? GroupInfoSearchNavigationContentNode { + return current + } else { + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + return GroupInfoSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel) + } + } + + func node(current: ItemListControllerSearchNode?) -> ItemListControllerSearchNode { + return GroupInfoSearchItemNode(account: self.account, peerId: self.peerId, openPeer: self.openPeer, cancel: self.cancel) + } +} + +private final class GroupInfoSearchItemNode: ItemListControllerSearchNode { + private let containerNode: ChannelMembersSearchContainerNode + + init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void, cancel: @escaping () -> Void) { + self.containerNode = ChannelMembersSearchContainerNode(account: account, peerId: peerId, mode: .searchMembers, openPeer: { peer in + openPeer(peer) + }) + self.containerNode.cancel = { + cancel() + } + + super.init() + + self.addSubnode(self.containerNode) + } + + override func queryUpdated(_ query: String) { + self.containerNode.searchTextUpdated(text: query) + } + + override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) + self.containerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) { + return result + } + + return super.hitTest(point, with: event) + } +} diff --git a/TelegramUI/GroupInfoSearchNavigationContentNode.swift b/TelegramUI/GroupInfoSearchNavigationContentNode.swift new file mode 100644 index 0000000000..15ef2a0048 --- /dev/null +++ b/TelegramUI/GroupInfoSearchNavigationContentNode.swift @@ -0,0 +1,65 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +private let searchBarFont = Font.regular(14.0) + +final class GroupInfoSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + private let searchBar: SearchBarNode + + private var queryUpdated: ((String) -> Void)? + + init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: theme, strings: strings) + let placeholderText = strings.Conversation_SearchByName_Placeholder + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query in + self?.queryUpdated?(query) + } + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let searchBarFrame = CGRect(origin: CGPoint(), size: size) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} + diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index 4e0238bdfe..ab224f8ad7 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -71,6 +71,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.stackFromBottom = true self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true super.init(account: account, theme: theme, strings: strings) @@ -98,7 +99,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in - var hashtagQueryRange: Range? + var hashtagQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.hashtag] { hashtagQueryRange = range @@ -107,21 +108,15 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } if let range = hashtagQueryRange { - var inputText = textInputState.inputText + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) let replacementText = text + " " - inputText.replaceSubrange(range, with: replacementText) - guard let lowerBound = range.lowerBound.samePosition(in: inputText.utf16) else { - return textInputState - } - let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: lowerBound) + inputText.replaceCharacters(in: range, with: replacementText) - let replacementLength = replacementText.utf16.distance(from: replacementText.utf16.startIndex, to: replacementText.utf16.endIndex) + let selectionPosition = range.lowerBound + (replacementText as NSString).length - let utfUpperPosition = utfLowerIndex + replacementLength - - return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) + return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) } return textInputState } diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index a36c161dd0..0d89d43d33 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -424,7 +424,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti var leftNavigationButton: ItemListNavigationButton? if case .modal = mode { - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } @@ -432,13 +432,13 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti var rightNavigationButton: ItemListNavigationButton? if let packCount = packCount, packCount != 0 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditing(true) } diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index 6991198176..ba04fa3247 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -72,7 +72,9 @@ final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, statusBar: self.statusBar, present: { [weak self] c, a in + self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, statusBar: self.statusBar, getNavigationController: { [weak self] in + return self?.navigationController as? NavigationController + }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, pushController: { [weak self] c in (self?.navigationController as? NavigationController)?.pushViewController(c) diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index f6ff2f4e27..2df8dfda0e 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -12,6 +12,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var presentationTheme: PresentationTheme private var strings: PresentationStrings private var theme: InstantPageTheme? + private let getNavigationController: () -> NavigationController? private let present: (ViewController, Any?) -> Void private let pushController: (ViewController) -> Void private let openPeer: (PeerId) -> Void @@ -46,7 +47,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let resolveUrlDisposable = MetaDisposable() private let loadWebpageDisposable = MetaDisposable() - init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, statusBar: StatusBar, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, statusBar: StatusBar, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.account = account self.presentationTheme = presentationTheme self.strings = strings @@ -54,6 +55,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.theme = settings.flatMap { return instantPageThemeForSettingsAndTime(settings: $0, time: Date()) } self.statusBar = statusBar + self.getNavigationController = getNavigationController self.present = present self.pushController = pushController self.openPeer = openPeer @@ -692,7 +694,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.resolveUrlDisposable.set((resolveUrl(account: self.account, url: url.url) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { - case let .externalUrl(externalUrl): if let webpageId = url.webpageId { var anchor: String? @@ -705,18 +706,30 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } })) } else { - strongSelf.account.telegramApplicationContext.applicationBindings.openUrl(externalUrl) + openExternalUrl(url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController()) } default: - break - /*case let .peer(peerId): - strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) - case let .botStart(peerId, payload): - strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)), fromMessageId: nil) - case let .groupBotStart(peerId, payload): - break - case let .channelMessage(peerId, messageId): - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId))*/ + openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, navigation in + switch navigation { + case let .chat(_, messageId): + if let navigationController = strongSelf.getNavigationController() { + navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), messageId: messageId) + } + case .info: + let _ = (strongSelf.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self { + if let controller = peerInfoController(account: strongSelf.account, peer: peer) { + strongSelf.getNavigationController()?.pushViewController(controller) + } + } + }) + case .withBotStartPayload: + break + } + }, present: { c, a in + self?.present(c, a) + }) } } })) diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index e735659421..6160f678b6 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -110,8 +110,18 @@ final class InstantPageTextItem: InstantPageItem { private func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? { let transformedPoint = CGPoint(x: point.x, y: point.y) - for line in self.lines { - let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + let boundsWidth = self.frame.width + for i in 0 ..< self.lines.count { + let line = self.lines[i] + + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } if lineFrame.contains(transformedPoint) { var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) if index == attributedString.length { @@ -129,8 +139,18 @@ final class InstantPageTextItem: InstantPageItem { break } } - for line in self.lines { - let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + for i in 0 ..< self.lines.count { + let line = self.lines[i] + + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + if lineFrame.insetBy(dx: -5.0, dy: -5.0).contains(transformedPoint) { var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) if index == attributedString.length { diff --git a/TelegramUI/ItemListCheckboxItem.swift b/TelegramUI/ItemListCheckboxItem.swift index 218171eb5c..979878db47 100644 --- a/TelegramUI/ItemListCheckboxItem.swift +++ b/TelegramUI/ItemListCheckboxItem.swift @@ -3,17 +3,24 @@ import Display import AsyncDisplayKit import SwiftSignalKit +enum ItemListCheckboxItemStyle { + case left + case right +} + class ItemListCheckboxItem: ListViewItem, ItemListItem { let theme: PresentationTheme let title: String + let style: ItemListCheckboxItemStyle let checked: Bool let zeroSeparatorInsets: Bool let sectionId: ItemListSectionId let action: () -> Void - init(theme: PresentationTheme, title: String, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + init(theme: PresentationTheme, title: String, style: ItemListCheckboxItemStyle, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.theme = theme self.title = title + self.style = style self.checked = checked self.zeroSeparatorInsets = zeroSeparatorInsets self.sectionId = sectionId @@ -107,7 +114,14 @@ class ItemListCheckboxItemNode: ListViewItemNode { let currentItem = self.item return { item, params, neighbors in - let leftInset: CGFloat = 44.0 + params.leftInset + var leftInset: CGFloat = params.leftInset + + switch item.style { + case .left: + leftInset += 44.0 + case .right: + leftInset += 16.0 + } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -117,7 +131,6 @@ class ItemListCheckboxItemNode: ListViewItemNode { let contentSize = CGSize(width: params.width, height: 44.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size var updateCheckImage: UIImage? var updatedTheme: PresentationTheme? @@ -145,7 +158,12 @@ class ItemListCheckboxItemNode: ListViewItemNode { let _ = titleApply() if let image = strongSelf.iconNode.image { - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + switch item.style { + case .left: + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + case .right: + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + } } strongSelf.iconNode.isHidden = !item.checked diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index d32eabfbd5..8391831a61 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -18,8 +18,41 @@ enum ItemListNavigationButtonStyle { } } +enum ItemListNavigationButtonContentIcon { + case search +} + +enum ItemListNavigationButtonContent: Equatable { + case none + case text(String) + case icon(ItemListNavigationButtonContentIcon) + + static func ==(lhs: ItemListNavigationButtonContent, rhs: ItemListNavigationButtonContent) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .text(value): + if case .text(value) = rhs { + return true + } else { + return false + } + case let .icon(value): + if case .icon(value) = rhs { + return true + } else { + return false + } + } + } +} + struct ItemListNavigationButton { - let title: String + let content: ItemListNavigationButtonContent let style: ItemListNavigationButtonStyle let enabled: Bool let action: () -> Void @@ -76,15 +109,17 @@ struct ItemListControllerState { let title: ItemListControllerTitle let leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton? + let secondaryRightNavigationButton: ItemListNavigationButton? let backNavigationButton: ItemListBackButton? let tabBarItem: ItemListControllerTabBarItem? let animateChanges: Bool - init(theme: PresentationTheme, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) { + init(theme: PresentationTheme, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, secondaryRightNavigationButton: ItemListNavigationButton? = nil, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) { self.theme = theme self.title = title self.leftNavigationButton = leftNavigationButton self.rightNavigationButton = rightNavigationButton + self.secondaryRightNavigationButton = secondaryRightNavigationButton self.backNavigationButton = backNavigationButton self.tabBarItem = tabBarItem self.animateChanges = animateChanges @@ -94,11 +129,11 @@ struct ItemListControllerState { final class ItemListController: ViewController { private let state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError> - private var leftNavigationButtonTitleAndStyle: (String, ItemListNavigationButtonStyle)? - private var rightNavigationButtonTitleAndStyle: (String, ItemListNavigationButtonStyle)? + private var leftNavigationButtonTitleAndStyle: (ItemListNavigationButtonContent, ItemListNavigationButtonStyle)? + private var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle)] = [] private var backNavigationButton: ItemListBackButton? private var tabBarItemInfo: ItemListControllerTabBarItem? - private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?) = (nil, nil) + private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil) private var segmentedTitleView: ItemListControllerSegmentedTitleView? private var theme: PresentationTheme @@ -192,12 +227,25 @@ final class ItemListController: ViewController { } } } - strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action) + strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action) - if strongSelf.leftNavigationButtonTitleAndStyle?.0 != controllerState.leftNavigationButton?.title || strongSelf.leftNavigationButtonTitleAndStyle?.1 != controllerState.leftNavigationButton?.style { + if strongSelf.leftNavigationButtonTitleAndStyle?.0 != controllerState.leftNavigationButton?.content || strongSelf.leftNavigationButtonTitleAndStyle?.1 != controllerState.leftNavigationButton?.style { if let leftNavigationButton = controllerState.leftNavigationButton { - let item = UIBarButtonItem(title: leftNavigationButton.title, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) - strongSelf.leftNavigationButtonTitleAndStyle = (leftNavigationButton.title, leftNavigationButton.style) + let item: UIBarButtonItem + switch leftNavigationButton.content { + case .none: + item = UIBarButtonItem(title: "", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + case let .text(value): + item = UIBarButtonItem(title: value, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + case let .icon(icon): + var image: UIImage? + switch icon { + case .search: + image = PresentationResourcesRootController.navigationSearchIcon(controllerState.theme) + } + item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + } + strongSelf.leftNavigationButtonTitleAndStyle = (leftNavigationButton.content, leftNavigationButton.style) strongSelf.navigationItem.setLeftBarButton(item, animated: false) item.isEnabled = leftNavigationButton.enabled } else { @@ -208,23 +256,62 @@ final class ItemListController: ViewController { barButtonItem.isEnabled = leftNavigationButton.enabled } - if strongSelf.rightNavigationButtonTitleAndStyle?.0 != controllerState.rightNavigationButton?.title || strongSelf.rightNavigationButtonTitleAndStyle?.1 != controllerState.rightNavigationButton?.style { - if let rightNavigationButton = controllerState.rightNavigationButton { + var rightNavigationButtonTitleAndStyle: [(ItemListNavigationButtonContent, ItemListNavigationButtonStyle, Bool)] = [] + if let secondaryRightNavigationButton = controllerState.secondaryRightNavigationButton { + rightNavigationButtonTitleAndStyle.append((secondaryRightNavigationButton.content, secondaryRightNavigationButton.style, secondaryRightNavigationButton.enabled)) + } + if let rightNavigationButton = controllerState.rightNavigationButton { + rightNavigationButtonTitleAndStyle.append((rightNavigationButton.content, rightNavigationButton.style, rightNavigationButton.enabled)) + } + + var updateRightButtonItems = false + if rightNavigationButtonTitleAndStyle.count != strongSelf.rightNavigationButtonTitleAndStyle.count { + updateRightButtonItems = true + } else { + for i in 0 ..< rightNavigationButtonTitleAndStyle.count { + if rightNavigationButtonTitleAndStyle[i].0 != strongSelf.rightNavigationButtonTitleAndStyle[i].0 || rightNavigationButtonTitleAndStyle[i].1 != strongSelf.rightNavigationButtonTitleAndStyle[i].1 { + updateRightButtonItems = true + } + } + } + + if updateRightButtonItems { + strongSelf.rightNavigationButtonTitleAndStyle = rightNavigationButtonTitleAndStyle.map { ($0.0, $0.1) } + var items: [UIBarButtonItem] = [] + var index = 0 + for (content, style, _) in rightNavigationButtonTitleAndStyle { let item: UIBarButtonItem - if case .activity = rightNavigationButton.style { + if case .activity = style { item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: controllerState.theme)) } else { - item = UIBarButtonItem(title: rightNavigationButton.title, style: rightNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed)) + let action: Selector = (index == 0 && rightNavigationButtonTitleAndStyle.count > 1) ? #selector(strongSelf.secondaryRightNavigationButtonPressed) : #selector(strongSelf.rightNavigationButtonPressed) + switch content { + case .none: + item = UIBarButtonItem(title: "", style: style.barButtonItemStyle, target: strongSelf, action: action) + case let .text(value): + item = UIBarButtonItem(title: value, style: style.barButtonItemStyle, target: strongSelf, action: action) + case let .icon(icon): + var image: UIImage? + switch icon { + case .search: + image = PresentationResourcesRootController.navigationSearchIcon(controllerState.theme) + } + item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) + } } - strongSelf.rightNavigationButtonTitleAndStyle = (rightNavigationButton.title, rightNavigationButton.style) - strongSelf.navigationItem.setRightBarButton(item, animated: false) - item.isEnabled = rightNavigationButton.enabled - } else { - strongSelf.rightNavigationButtonTitleAndStyle = nil - strongSelf.navigationItem.setRightBarButton(nil, animated: false) + items.append(item) + index += 1 + } + strongSelf.navigationItem.setRightBarButtonItems(items, animated: false) + index = 0 + for (_, _, enabled) in rightNavigationButtonTitleAndStyle { + items[index].isEnabled = enabled + index += 1 + } + } else { + for i in 0 ..< rightNavigationButtonTitleAndStyle.count { + strongSelf.navigationItem.rightBarButtonItems?[i].isEnabled = rightNavigationButtonTitleAndStyle[i].2 } - } else if let barButtonItem = strongSelf.navigationItem.rightBarButtonItem, let rightNavigationButton = controllerState.rightNavigationButton, rightNavigationButton.enabled != barButtonItem.isEnabled { - barButtonItem.isEnabled = rightNavigationButton.enabled } if strongSelf.backNavigationButton != controllerState.backNavigationButton { @@ -245,20 +332,18 @@ final class ItemListController: ViewController { strongSelf.segmentedTitleView?.color = controllerState.theme.rootController.navigationBar.accentTextColor - if let rightNavigationButton = controllerState.rightNavigationButton { - if case .activity = rightNavigationButton.style { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: controllerState.theme))! - - strongSelf.rightNavigationButtonTitleAndStyle = (rightNavigationButton.title, rightNavigationButton.style) - strongSelf.navigationItem.setRightBarButton(item, animated: false) - item.isEnabled = rightNavigationButton.enabled + var items = strongSelf.navigationItem.rightBarButtonItems ?? [] + for i in 0 ..< strongSelf.rightNavigationButtonTitleAndStyle.count { + if case .activity = strongSelf.rightNavigationButtonTitleAndStyle[i].1 { + items[i] = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: controllerState.theme))! } } + strongSelf.navigationItem.setRightBarButtonItems(items, animated: false) } } } } |> map { ($0.theme, $1) } - let displayNode = ItemListControllerNode(updateNavigationOffset: { [weak self] offset in + let displayNode = ItemListControllerNode(navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in if let strongSelf = self { strongSelf.navigationOffset = offset } @@ -286,6 +371,10 @@ final class ItemListController: ViewController { self.navigationButtonActions.right?() } + @objc func secondaryRightNavigationButtonPressed() { + self.navigationButtonActions.secondaryRight?() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index d04a6ce8c3..dbaad39e65 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -39,6 +39,7 @@ private struct ItemListNodeTransition { let entries: ItemListNodeEntryTransition let updateStyle: ItemListStyle? let emptyStateItem: ItemListControllerEmptyStateItem? + let searchItem: ItemListControllerSearch? let focusItemTag: ItemListItemTag? let firstTime: Bool let animated: Bool @@ -50,13 +51,15 @@ struct ItemListNodeState { let entries: [Entry] let style: ItemListStyle let emptyStateItem: ItemListControllerEmptyStateItem? + let searchItem: ItemListControllerSearch? let animateChanges: Bool let focusItemTag: ItemListItemTag? - init(entries: [Entry], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, animateChanges: Bool = true) { + init(entries: [Entry], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, animateChanges: Bool = true) { self.entries = entries self.style = style self.emptyStateItem = emptyStateItem + self.searchItem = searchItem self.animateChanges = animateChanges self.focusItemTag = focusItemTag } @@ -84,17 +87,22 @@ final class ItemListNodeVisibleEntries: Sequence { } } -class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { +class ItemListControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private var _ready = ValuePromise() public var ready: Signal { return self._ready.get() } private var didSetReady = false + private let navigationBar: NavigationBar + let listNode: ListView private var emptyStateItem: ItemListControllerEmptyStateItem? private var emptyStateNode: ItemListControllerEmptyStateItemNode? + private var searchItem: ItemListControllerSearch? + private var searchNode: ItemListControllerSearchNode? + private let transitionDisposable = MetaDisposable() private var enqueuedTransitions: [ItemListNodeTransition] = [] @@ -113,17 +121,14 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV } } - init(updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(PresentationTheme, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { + init(navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(PresentationTheme, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { + self.navigationBar = navigationBar self.updateNavigationOffset = updateNavigationOffset self.listNode = ListView() super.init() - self.setViewBlock({ - return UITracingLayerView() - }) - self.backgroundColor = nil self.isOpaque = false @@ -158,7 +163,7 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV if previous?.style != state.style { updatedStyle = state.style } - return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, mergedEntries: state.entries) + return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, mergedEntries: state.entries) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.enqueueTransition(transition) @@ -224,6 +229,10 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV emptyStateNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) } + if let searchNode = self.searchNode { + searchNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + let dequeue = self.validLayout == nil self.validLayout = (layout, navigationBarHeight) if dequeue { @@ -326,6 +335,51 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.emptyStateNode = nil } } + + var updateSearchItem = false + if let searchItem = self.searchItem, let updatedSearchItem = transition.searchItem { + updateSearchItem = !searchItem.isEqual(to: updatedSearchItem) + } else if (self.searchItem != nil) != (transition.searchItem != nil) { + updateSearchItem = true + } + if updateSearchItem { + self.searchItem = transition.searchItem + if let searchItem = transition.searchItem { + let updatedNode = searchItem.node(current: self.searchNode) + if let searchNode = self.searchNode, updatedNode !== searchNode { + searchNode.removeFromSupernode() + } + if self.searchNode !== updatedNode { + self.searchNode = updatedNode + if let validLayout = self.validLayout { + updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate) + } + self.insertSubnode(updatedNode, belowSubnode: self.navigationBar) + updatedNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut) + } + let updatedTitleContentNode = searchItem.titleContentNode(current: self.navigationBar.contentNode as? (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)) + if updatedTitleContentNode !== self.navigationBar.contentNode { + updatedTitleContentNode.setQueryUpdated { [weak self] query in + if let strongSelf = self { + strongSelf.searchNode?.queryUpdated(query) + } + } + self.navigationBar.setContentNode(updatedTitleContentNode, animated: true) + updatedTitleContentNode.activate() + } + } else { + if let searchNode = self.searchNode { + self.searchNode = nil + searchNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak searchNode]_ in + searchNode?.removeFromSupernode() + }) + } + + if let _ = self.navigationBar.contentNode { + self.navigationBar.setContentNode(nil, animated: true) + } + } + } } } @@ -354,4 +408,14 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.animateOut() } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let searchNode = self.searchNode { + if let result = searchNode.hitTest(point, with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } } diff --git a/TelegramUI/ItemListControllerSearch.swift b/TelegramUI/ItemListControllerSearch.swift new file mode 100644 index 0000000000..5ac9b3372d --- /dev/null +++ b/TelegramUI/ItemListControllerSearch.swift @@ -0,0 +1,27 @@ +import Foundation +import AsyncDisplayKit +import Display + +protocol ItemListControllerSearchNavigationContentNode { + func activate() + func deactivate() + + func setQueryUpdated(_ f: @escaping (String) -> Void) +} + +protocol ItemListControllerSearch { + func isEqual(to: ItemListControllerSearch) -> Bool + func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode + func node(current: ItemListControllerSearchNode?) -> ItemListControllerSearchNode +} + +class ItemListControllerSearchNode: ASDisplayNode { + func queryUpdated(_ query: String) { + + } + + func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + + } +} + diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index e2c4b4b775..81ebd24784 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -81,6 +81,7 @@ class ItemListMultilineTextItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(17.0) private let titleBoldFont = Font.medium(17.0) +private let titleItalicFont = Font.italic(17.0) private let titleFixedFont = Font.regular(17.0) class ItemListMultilineTextItemNode: ListViewItemNode { @@ -174,7 +175,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode { } let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) - let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, boldFont: titleBoldFont, fixedFont: titleFixedFont) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleBoldFont, italicFont: titleItalicFont, fixedFont: titleFixedFont) let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index ad0ac1a2f6..8e9b21deac 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -36,16 +36,32 @@ enum ItemListPeerItemLabel { case disclosure(String) } +struct ItemListPeerItemSwitch { + let value: Bool + let style: ItemListPeerItemSwitchStyle +} + +enum ItemListPeerItemSwitchStyle { + case standard + case check +} + +enum ItemListPeerItemAliasHandling { + case standard + case threatSelfAsSaved +} + final class ItemListPeerItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings let account: Account let peer: Peer + let aliasHandling: ItemListPeerItemAliasHandling let presence: PeerPresence? let text: ItemListPeerItemText let label: ItemListPeerItemLabel let editing: ItemListPeerItemEditing - let switchValue: Bool? + let switchValue: ItemListPeerItemSwitch? let enabled: Bool let sectionId: ItemListSectionId let action: (() -> Void)? @@ -53,11 +69,12 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: Bool?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account self.peer = peer + self.aliasHandling = aliasHandling self.presence = presence self.text = text self.label = label @@ -135,6 +152,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private var labelArrowNode: ASImageNode? private let statusNode: TextNode private var switchNode: SwitchNode? + private var checkNode: ASImageNode? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors)? @@ -207,6 +225,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { var currentDisabledOverlayNode = self.disabledOverlayNode var currentSwitchNode = self.switchNode + var currentCheckNode = self.checkNode let currentLabelArrowNode = self.labelArrowNode @@ -234,17 +253,32 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { var rightInset: CGFloat = params.rightInset let switchSize = CGSize(width: 51.0, height: 31.0) + var checkImage: UIImage? - if let _ = item.switchValue { - if currentSwitchNode == nil { - currentSwitchNode = SwitchNode() + if let switchValue = item.switchValue { + switch switchValue.style { + case .standard: + if currentSwitchNode == nil { + currentSwitchNode = SwitchNode() + } + rightInset += switchSize.width + currentCheckNode = nil + case .check: + checkImage = PresentationResourcesItemList.checkIconImage(item.theme) + if currentCheckNode == nil { + currentCheckNode = ASImageNode() + } + rightInset += 10.0 + currentSwitchNode = nil } - rightInset += switchSize.width } else { currentSwitchNode = nil + currentCheckNode = nil } - if let user = item.peer as? TelegramUser { + if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling { + titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) + } else if let user = item.peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: firstName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) @@ -461,13 +495,34 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } currentSwitchNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) if let switchValue = item.switchValue { - currentSwitchNode.setOn(switchValue, animated: animated) + currentSwitchNode.setOn(switchValue.value, animated: animated) } } else if let switchNode = strongSelf.switchNode { switchNode.removeFromSupernode() strongSelf.switchNode = nil } + if let currentCheckNode = currentCheckNode { + if currentCheckNode !== strongSelf.checkNode { + strongSelf.checkNode = currentCheckNode + if let disabledOverlayNode = strongSelf.disabledOverlayNode, disabledOverlayNode.supernode != nil { + strongSelf.insertSubnode(currentCheckNode, belowSubnode: disabledOverlayNode) + } else { + strongSelf.addSubnode(currentCheckNode) + } + } + if let checkImage = checkImage { + currentCheckNode.image = checkImage + currentCheckNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - checkImage.size.width - floor((44.0 - checkImage.size.width) / 2.0), y: floor((layout.contentSize.height - checkImage.size.height) / 2.0)), size: checkImage.size) + } + if let switchValue = item.switchValue { + currentCheckNode.isHidden = !switchValue.value + } + } else if let checkNode = strongSelf.checkNode { + checkNode.removeFromSupernode() + strongSelf.checkNode = nil + } + var rightLabelInset: CGFloat = 15.0 if let updatedLabelArrowNode = updatedLabelArrowNode { @@ -486,7 +541,12 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)) transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) - strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) + + if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling { + strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer, overrideImage: .savedMessagesIcon) + } else { + strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) + } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 48.0 + UIScreenPixel + UIScreenPixel)) diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index d1e04f16ab..9bde8160f7 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -73,6 +73,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { private let labelFont = Font.regular(14.0) private let textFont = Font.regular(17.0) private let textBoldFont = Font.medium(17.0) +private let textItalicFont = Font.italic(17.0) private let textFixedFont = Font.regular(17.0) class ItemListTextWithLabelItemNode: ListViewItemNode { @@ -158,7 +159,7 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) - let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, boldFont: textBoldFont, fixedFont: textFixedFont) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: textLayout.size.height + 39.0) diff --git a/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift b/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift index 54c5e15902..a88a8da523 100644 --- a/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift +++ b/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift @@ -49,7 +49,7 @@ final class LegacyPeerAvatarPlaceholderDataSource: TGImageDataSource { if let account = self.account() { let task = ThreadPoolTask { state in let args: [AnyHashable : Any] - let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "placeholder://?".characters.count)) + let argumentsString = String(uri[uri.index(uri.startIndex, offsetBy: "placeholder://?".characters.count)...]) args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! guard let width = Int((args["w"] as! String)), width > 1 else { @@ -63,6 +63,8 @@ final class LegacyPeerAvatarPlaceholderDataSource: TGImageDataSource { if let uid = args["uid"] as? String, let nUid = Int32(uid) { peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: nUid) + } else if let cid = args["cid"] as? String, let nCid = Int32(cid) { + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: nCid) } let image = generateImage(CGSize(width: CGFloat(width), height: CGFloat(height)), rotatedContext: { size, context in diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 42952cc7c1..491cc7a65f 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -770,7 +770,7 @@ final class ListMessageFileItemNode: ListMessageNode { } case .Local: if let item = self.item, let controllerInteraction = self.controllerInteraction { - let _ = controllerInteraction.openMessage(item.message.id) + let _ = controllerInteraction.openMessage(item.message) } } case .playbackStatus: @@ -796,7 +796,7 @@ final class ListMessageFileItemNode: ListMessageNode { override func longTapped() { if let item = self.item { - item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.bounds) + item.controllerInteraction.openMessageContextMenu(item.message, self, self.bounds) } } } diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index d5d43fdf64..0d2b24a3fe 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -409,9 +409,9 @@ final class ListMessageSnippetItemNode: ListMessageNode { func activateMedia() { if let item = self.item, let currentPrimaryUrl = self.currentPrimaryUrl { if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { - item.controllerInteraction.openInstantPage(item.message.id) + item.controllerInteraction.openInstantPage(item.message) } else { - if !item.controllerInteraction.openMessage(item.message.id) { + if !item.controllerInteraction.openMessage(item.message) { item.controllerInteraction.openUrl(currentPrimaryUrl) } } @@ -461,7 +461,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { if case .longTap = gesture { item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url)) } else if url == self.currentPrimaryUrl { - item.controllerInteraction.openMessage(item.message.id) + item.controllerInteraction.openMessage(item.message) } else { item.controllerInteraction.openUrl(url) } @@ -517,7 +517,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { override func longTapped() { if let item = self.item { - item.controllerInteraction.openMessageContextMenu(item.message.id, self, self.bounds) + item.controllerInteraction.openMessageContextMenu(item.message, self, self.bounds) } } } diff --git a/TelegramUI/LiveLocationManager.swift b/TelegramUI/LiveLocationManager.swift index b3c55d742f..70d009196d 100644 --- a/TelegramUI/LiveLocationManager.swift +++ b/TelegramUI/LiveLocationManager.swift @@ -31,11 +31,13 @@ public final class LiveLocationManager { private let deviceLocationDisposable = MetaDisposable() private var messagesDisposable: Disposable? - private var broadcastToMessageIds = Set() + private var broadcastToMessageIds: [MessageId: Int32] = [:] private var stopMessageIds = Set() private let editMessageDisposables = DisposableDict() + private var invalidationTimer: (SwiftSignalKit.Timer, Int32)? + init(postbox: Postbox, network: Network, accountPeerId: PeerId, viewTracker: AccountViewTracker, stateManager: AccountStateManager, locationManager: DeviceLocationManager, inForeground: Signal) { self.postbox = postbox self.network = network @@ -50,7 +52,7 @@ public final class LiveLocationManager { if let strongSelf = self { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - var broadcastToMessageIds = Set() + var broadcastToMessageIds: [MessageId: Int32] = [:] var stopMessageIds = Set() if let view = view.views[viewKey] as? LocalMessageTagsView { @@ -67,8 +69,8 @@ public final class LiveLocationManager { } } } - if let _ = activeLiveBroadcastingTimeout { - broadcastToMessageIds.insert(message.id) + if let activeLiveBroadcastingTimeout = activeLiveBroadcastingTimeout { + broadcastToMessageIds[message.id] = message.timestamp + activeLiveBroadcastingTimeout } else { stopMessageIds.insert(message.id) } @@ -117,23 +119,25 @@ public final class LiveLocationManager { self.deviceLocationDisposable.dispose() self.messagesDisposable?.dispose() self.editMessageDisposables.dispose() + self.invalidationTimer?.0.invalidate() } - private func update(broadcastToMessageIds: Set, stopMessageIds: Set) { + private func update(broadcastToMessageIds: [MessageId: Int32], stopMessageIds: Set) { assert(self.queue.isCurrent()) if self.broadcastToMessageIds == broadcastToMessageIds && self.stopMessageIds == stopMessageIds { return } + let validBroadcastToMessageIds = Set(broadcastToMessageIds.keys) if self.broadcastToMessageIds != broadcastToMessageIds { - self.summaryManager.update(messageIds: broadcastToMessageIds) + self.summaryManager.update(messageIds: validBroadcastToMessageIds) } let wasEmpty = self.broadcastToMessageIds.isEmpty self.broadcastToMessageIds = broadcastToMessageIds - let removedFromActions = self.broadcastToMessageIds.union(self.stopMessageIds).subtracting(broadcastToMessageIds.union(stopMessageIds)) + let removedFromActions = Set(self.broadcastToMessageIds.keys).union(self.stopMessageIds).subtracting(validBroadcastToMessageIds.union(stopMessageIds)) for id in removedFromActions { self.editMessageDisposables.set(nil, forKey: id) } @@ -156,14 +160,54 @@ public final class LiveLocationManager { } }), forKey: id) } + + self.rescheduleTimer() + } + + private func rescheduleTimer() { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + var updatedBroadcastToMessageIds = self.broadcastToMessageIds + var updatedStopMessageIds = self.stopMessageIds + + var earliestCancelIdAndTimestamp: (MessageId, Int32)? + for (id, timestamp) in self.broadcastToMessageIds { + if currentTimestamp >= timestamp { + updatedBroadcastToMessageIds.removeValue(forKey: id) + updatedStopMessageIds.insert(id) + } else { + if earliestCancelIdAndTimestamp == nil || timestamp < earliestCancelIdAndTimestamp!.1 { + earliestCancelIdAndTimestamp = (id, timestamp) + } + } + } + + if let (_, timestamp) = earliestCancelIdAndTimestamp { + if self.invalidationTimer?.1 != timestamp { + self.invalidationTimer?.0.invalidate() + + let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timestamp - currentTimestamp)), repeat: false, completion: { [weak self] in + self?.invalidationTimer?.0.invalidate() + self?.invalidationTimer = nil + self?.rescheduleTimer() + }, queue: self.queue) + self.invalidationTimer = (timer, timestamp) + timer.start() + } + } else if let (timer, _) = self.invalidationTimer { + self.invalidationTimer = nil + timer.invalidate() + } + + self.update(broadcastToMessageIds: updatedBroadcastToMessageIds, stopMessageIds: updatedStopMessageIds) } private func updateDeviceCoordinate(_ coordinate: CLLocationCoordinate2D) { assert(self.queue.isCurrent()) let ids = self.broadcastToMessageIds - let remainingIds = Atomic>(value: ids) - for id in ids { + let remainingIds = Atomic>(value: Set(ids.keys)) + for id in ids.keys { self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude)) |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { @@ -185,7 +229,7 @@ public final class LiveLocationManager { func cancelLiveLocation(peerId: PeerId) { assert(self.queue.isCurrent()) - let ids = self.broadcastToMessageIds.filter({ $0.peerId == peerId }) + let ids = self.broadcastToMessageIds.keys.filter({ $0.peerId == peerId }) if !ids.isEmpty { let _ = self.postbox.modify({ modifier -> Void in for id in ids { @@ -215,7 +259,7 @@ public final class LiveLocationManager { } func internalMessageForPeerId(_ peerId: PeerId) -> MessageId? { - for id in self.broadcastToMessageIds { + for id in self.broadcastToMessageIds.keys { if id.peerId == peerId { return id } diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index e2497cd414..496da6a496 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -35,10 +35,10 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { self.listNode = nil } if let updatedPlaylistPeerId = updatedPlaylistPeerId { - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in if let strongSelf = self, let listNode = strongSelf.listNode { var galleryMedia: Media? - if let message = listNode.messageInCurrentHistoryView(id) { + if let message = listNode.messageInCurrentHistoryView(message.id) { for media in message.media { if let file = media as? TelegramMediaFile { galleryMedia = file @@ -57,7 +57,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { if let galleryMedia = galleryMedia { if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, account: strongSelf.account, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, account: strongSelf.account, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: message.id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -66,7 +66,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, automaticMediaDownloadSettings: .none) + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index 9a89d8dca8..eed9b6f636 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -18,7 +18,7 @@ private func generateHandleBackground(color: UIColor) -> UIImage? { })?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 2) } -private final class MediaPlayerScrubbingNodeButton: ASButtonNode { +private final class MediaPlayerScrubbingNodeButton: ASDisplayNode { var beginScrubbing: (() -> Void)? var endScrubbing: ((Bool) -> Void)? var updateScrubbing: ((CGFloat) -> Void)? @@ -29,49 +29,32 @@ private final class MediaPlayerScrubbingNodeButton: ASButtonNode { super.didLoad() self.view.disablesInteractiveTransitionGestureRecognizer = true + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } - override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool { - if super.beginTracking(with: touch, with: event) { - scrubbingStartLocation = touch.location(in: self.view) - self.beginScrubbing?() - return true - } else { - return false + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.scrubbingStartLocation = recognizer.location(in: self.view) + self.beginScrubbing?() + case .changed: + let location = recognizer.location(in: self.view) + if let scrubbingStartLocation = self.scrubbingStartLocation { + let delta = location.x - scrubbingStartLocation.x + self.updateScrubbing?(delta / self.bounds.size.width) + } + case .ended, .cancelled: + let location = recognizer.location(in: self.view) + if let scrubbingStartLocation = self.scrubbingStartLocation { + self.scrubbingStartLocation = nil + let delta = location.x - scrubbingStartLocation.x + self.updateScrubbing?(delta / self.bounds.size.width) + self.endScrubbing?(recognizer.state == .ended) + } + default: + break } } - - override func continueTracking(with touch: UITouch, with touchEvent: UIEvent?) -> Bool { - if super.continueTracking(with: touch, with: touchEvent) { - let location = touch.location(in: self.view) - if let scrubbingStartLocation = self.scrubbingStartLocation { - let delta = location.x - scrubbingStartLocation.x - self.updateScrubbing?(delta / self.bounds.size.width) - } - return true - } else { - return false - } - } - - override func endTracking(with touch: UITouch?, with event: UIEvent?) { - super.endTracking(with: touch, with: event) - if let touch = touch { - let location = touch.location(in: self.view) - if let scrubbingStartLocation = self.scrubbingStartLocation { - let delta = location.x - scrubbingStartLocation.x - self.updateScrubbing?(delta / self.bounds.size.width) - } - } - self.scrubbingStartLocation = nil - self.endScrubbing?(true) - } - - override func cancelTracking(with event: UIEvent?) { - super.cancelTracking(with: event) - self.scrubbingStartLocation = nil - self.endScrubbing?(false) - } } private final class MediaPlayerScrubbingForegroundNode: ASDisplayNode { @@ -115,9 +98,23 @@ private final class StandardMediaPlayerScrubbingNodeContentNode { } } +private final class CustomMediaPlayerScrubbingNodeContentNode { + let backgroundNode: ASDisplayNode + let foregroundContentNode: ASDisplayNode + let foregroundNode: MediaPlayerScrubbingForegroundNode + let handleNodeContainer: MediaPlayerScrubbingNodeButton? + + init(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handleNodeContainer: MediaPlayerScrubbingNodeButton?) { + self.backgroundNode = backgroundNode + self.foregroundContentNode = foregroundContentNode + self.foregroundNode = foregroundNode + self.handleNodeContainer = handleNodeContainer + } +} + private enum MediaPlayerScrubbingNodeContentNodes { case standard(StandardMediaPlayerScrubbingNodeContentNode) - case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode, foregroundNode: MediaPlayerScrubbingForegroundNode) + case custom(CustomMediaPlayerScrubbingNodeContentNode) } final class MediaPlayerScrubbingNode: ASDisplayNode { @@ -229,7 +226,9 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { foregroundNode.isLayerBacked = true foregroundNode.clipsToBounds = true - self.contentNodes = .custom(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode) + let handleNodeContainer = MediaPlayerScrubbingNodeButton() + + self.contentNodes = .custom(CustomMediaPlayerScrubbingNodeContentNode(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNodeContainer: handleNodeContainer)) } super.init() @@ -278,12 +277,12 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { node.foregroundNode.onEnterHierarchy = { [weak self] in self?.updateProgress() } - case let .custom(backgroundNode, foregroundContentNode, foregroundNode): - self.addSubnode(backgroundNode) - foregroundNode.addSubnode(foregroundContentNode) - self.addSubnode(foregroundNode) + case let .custom(node): + self.addSubnode(node.backgroundNode) + node.foregroundNode.addSubnode(node.foregroundContentNode) + self.addSubnode(node.foregroundNode) - /*if let handleNodeContainer = handleNodeContainer { + if let handleNodeContainer = node.handleNodeContainer { self.addSubnode(handleNodeContainer) handleNodeContainer.beginScrubbing = { [weak self] in if let strongSelf = self { @@ -313,9 +312,9 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { strongSelf.updateProgress() } } - }*/ + } - foregroundNode.onEnterHierarchy = { [weak self] in + node.foregroundNode.onEnterHierarchy = { [weak self] in self?.updateProgress() } } @@ -508,13 +507,17 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } else { } - case let .custom(backgroundNode, foregroundContentNode, foregroundNode): - foregroundNode.layer.removeAnimation(forKey: "playback-bounds") - foregroundNode.layer.removeAnimation(forKey: "playback-position") + case let .custom(node): + if let handleNodeContainer = node.handleNodeContainer { + handleNodeContainer.frame = bounds + } + + node.foregroundNode.layer.removeAnimation(forKey: "playback-bounds") + node.foregroundNode.layer.removeAnimation(forKey: "playback-position") let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: bounds.size.height)) - backgroundNode.frame = backgroundFrame - foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) + node.backgroundNode.frame = backgroundFrame + node.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) let timestampAndDuration: (timestamp: Double, duration: Double)? if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { @@ -536,9 +539,9 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - foregroundNode.frame = toRect - foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") - foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") } else if let statusValue = self.statusValue, !progress.isNaN && progress.isFinite { if statusValue.generationTimestamp.isZero { let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) @@ -547,9 +550,9 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - foregroundNode.frame = toRect - foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") - foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: nil, offset: timestamp, speed: 0.0), forKey: "playback-position") } else { let fromRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) let toRect = CGRect(origin: backgroundFrame.origin, size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) @@ -557,15 +560,15 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { let fromBounds = CGRect(origin: CGPoint(), size: fromRect.size) let toBounds = CGRect(origin: CGPoint(), size: toRect.size) - foregroundNode.frame = toRect - foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") - foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") + node.foregroundNode.frame = toRect + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "bounds", from: NSValue(cgRect: fromBounds), to: NSValue(cgRect: toBounds), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-bounds") + node.foregroundNode.layer.add(self.preparedAnimation(keyPath: "position", from: NSValue(cgPoint: CGPoint(x: fromRect.midX, y: fromRect.midY)), to: NSValue(cgPoint: CGPoint(x: toRect.midX, y: toRect.midY)), duration: duration, beginTime: statusValue.generationTimestamp, offset: timestamp, speed: statusValue.status == .playing ? 1.0 : 0.0), forKey: "playback-position") } } else { - foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) } } else { - foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) + node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height)) } } } @@ -575,8 +578,8 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { switch self.contentNodes { case let .standard(node): return node.handleNodeContainer?.view - case .custom: - break + case let .custom(node): + return node.handleNodeContainer?.view } return super.hitTest(point, with: event) } else { diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index 3446517df7..8341df4533 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -67,6 +67,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.stackFromBottom = true self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true super.init(account: account, theme: theme, strings: strings) @@ -100,7 +101,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { switch strongSelf.mode { case .input: interfaceInteraction.updateTextInputState { textInputState in - var mentionQueryRange: Range? + var mentionQueryRange: NSRange? inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.mention] { mentionQueryRange = range @@ -109,22 +110,28 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } if let range = mentionQueryRange { - var inputText = textInputState.inputText + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) if let addressName = peer.addressName, !addressName.isEmpty { let replacementText = addressName + " " - inputText.replaceSubrange(range, with: replacementText) - guard let lowerBound = range.lowerBound.samePosition(in: inputText.utf16) else { - return textInputState - } - let utfLowerIndex = inputText.utf16.distance(from: inputText.utf16.startIndex, to: lowerBound) + inputText.replaceCharacters(in: range, with: replacementText) - let replacementLength = replacementText.utf16.distance(from: replacementText.utf16.startIndex, to: replacementText.utf16.endIndex) + let selectionPosition = range.lowerBound + (replacementText as NSString).length - let utfUpperPosition = utfLowerIndex + replacementLength + return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + } else if !peer.compactDisplayTitle.isEmpty { + let replacementText = NSMutableAttributedString() + replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)])) + replacementText.append(NSAttributedString(string: " ")) - return ChatTextInputState(inputText: inputText, selectionRange: utfUpperPosition ..< utfUpperPosition) + let updatedRange = NSRange(location: range.location - 1, length: range.length + 1) + + inputText.replaceCharacters(in: updatedRange, with: replacementText) + + let selectionPosition = updatedRange.lowerBound + replacementText.length + + return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) } } return textInputState diff --git a/TelegramUI/MergeLists.swift b/TelegramUI/MergeLists.swift new file mode 100644 index 0000000000..493812cf21 --- /dev/null +++ b/TelegramUI/MergeLists.swift @@ -0,0 +1,333 @@ +import Foundation + +public protocol Identifiable { + associatedtype T: Hashable + var stableId: T { get } +} + +public func mergeListsStableWithUpdates(leftList: [T], rightList: [T]) -> ([Int], [(Int, T, Int?)], [(Int, T, Int)]) where T: Comparable, T: Identifiable { + var removeIndices: [Int] = [] + var insertItems: [(Int, T, Int?)] = [] + var updatedIndices: [(Int, T, Int)] = [] + + #if (arch(i386) || arch(x86_64)) && os(iOS) + var existingStableIds: [T.T: T] = [:] + for item in leftList { + if let _ = existingStableIds[item.stableId] { + assertionFailure() + } else { + existingStableIds[item.stableId] = item + } + } + existingStableIds.removeAll() + for item in rightList { + if let other = existingStableIds[item.stableId] { + print("\(other) has the same stableId as \(item): \(item.stableId)") + assertionFailure() + } else { + existingStableIds[item.stableId] = item + } + } + #endif + + var currentList = leftList + + var i = 0 + var previousIndices: [T.T: Int] = [:] + for left in leftList { + previousIndices[left.stableId] = i + i += 1 + } + + i = 0 + var j = 0 + while true { + let left: T? = i < currentList.count ? currentList[i] : nil + let right: T? = j < rightList.count ? rightList[j] : nil + + if let left = left, let right = right { + if left.stableId == right.stableId && left != right { + updatedIndices.append((i, right, previousIndices[left.stableId]!)) + i += 1 + j += 1 + } else { + if left == right { + i += 1 + j += 1 + } else if left < right { + removeIndices.append(i) + i += 1 + } else if !(left > right) { + removeIndices.append(i) + i += 1 + } else { + j += 1 + } + } + } else if let _ = left { + removeIndices.append(i) + i += 1 + } else if let _ = right { + j += 1 + } else { + break + } + } + + //print("remove:\n\(removeIndices)") + + for index in removeIndices.reversed() { + currentList.remove(at: index) + for i in 0 ..< updatedIndices.count { + if updatedIndices[i].0 >= index { + updatedIndices[i].0 -= 1 + } + } + } + + /*print("\n current after removes:\n") + m = 0 + for right in currentList { + print("\(m): \(right.stableId)") + m += 1 + } + + print("update:\n\(updatedIndices.map({ "\($0.0), \($0.1.stableId) (was \($0.2)))" }))")*/ + + i = 0 + j = 0 + var k = 0 + while true { + let left: T? + + //print("i=\(i), j=\(j), k=\(k)") + + if k < updatedIndices.count && updatedIndices[k].0 < i { + //print("updated[k=\(k)]=\(updatedIndices[k].0) right { + //print("\(left.stableId)>\(right.stableId)") + //print("insert \(right.stableId) at \(i)") + //print("i++, j++") + let previousIndex = previousIndices[right.stableId] + insertItems.append((i, right, previousIndex)) + currentList.insert(right, at: i) + if k < updatedIndices.count { + for l in k ..< updatedIndices.count { + updatedIndices[l] = (updatedIndices[l].0 + 1, updatedIndices[l].1, updatedIndices[l].2) + } + } + + i += 1 + j += 1 + } else { + //print("\(left.stableId)<\(right.stableId)") + //print("i++") + i += 1 + } + } else if let _ = left { + //print("\(left!.stableId)>nil") + //print("i++") + i += 1 + } else if let right = right { + //print("nil<\(right.stableId)") + //print("insert \(right.stableId) at \(i)") + //print("i++") + //print("j++") + let previousIndex = previousIndices[right.stableId] + insertItems.append((i, right, previousIndex)) + currentList.insert(right, at: i) + + if k < updatedIndices.count { + for l in k ..< updatedIndices.count { + updatedIndices[l] = (updatedIndices[l].0 + 1, updatedIndices[l].1, updatedIndices[l].2) + } + } + + i += 1 + j += 1 + } else { + break + } + } + + for (index, item, _) in updatedIndices { + currentList[index] = item + } + + assert(currentList == rightList, "currentList == rightList") + + return (removeIndices, insertItems, updatedIndices) +} + +public func mergeListsStableWithUpdatesReversed(leftList: [T], rightList: [T]) -> ([Int], [(Int, T, Int?)], [(Int, T, Int)]) where T: Comparable, T: Identifiable { + var removeIndices: [Int] = [] + var insertItems: [(Int, T, Int?)] = [] + var updatedIndices: [(Int, T, Int)] = [] + + #if (arch(i386) || arch(x86_64)) && os(iOS) + var existingStableIds: [T.T: T] = [:] + for item in leftList { + if let _ = existingStableIds[item.stableId] { + assertionFailure() + } else { + existingStableIds[item.stableId] = item + } + } + existingStableIds.removeAll() + for item in rightList { + if let other = existingStableIds[item.stableId] { + print("\(other) has the same stableId as \(item): \(item.stableId)") + assertionFailure() + } else { + existingStableIds[item.stableId] = item + } + } + #endif + + var currentList = leftList + + var i = 0 + var previousIndices: [T.T: Int] = [:] + for left in leftList { + previousIndices[left.stableId] = i + i += 1 + } + + i = 0 + var j = 0 + while true { + let left: T? = i < currentList.count ? currentList[i] : nil + let right: T? = j < rightList.count ? rightList[j] : nil + + if let left = left, let right = right { + if left.stableId == right.stableId && left != right { + updatedIndices.append((i, right, previousIndices[left.stableId]!)) + i += 1 + j += 1 + } else { + if left == right { + i += 1 + j += 1 + } else if left > right { + removeIndices.append(i) + i += 1 + } else if !(left < right) { + removeIndices.append(i) + i += 1 + } else { + j += 1 + } + } + } else if let _ = left { + removeIndices.append(i) + i += 1 + } else if let _ = right { + j += 1 + } else { + break + } + } + + //print("remove:\n\(removeIndices)") + + for index in removeIndices.reversed() { + currentList.remove(at: index) + for i in 0 ..< updatedIndices.count { + if updatedIndices[i].0 >= index { + updatedIndices[i].0 -= 1 + } + } + } + + i = 0 + j = 0 + var k = 0 + while true { + let left: T? + + if k < updatedIndices.count && updatedIndices[k].0 < i { + k += 1 + } + + if k < updatedIndices.count { + if updatedIndices[k].0 == i { + left = updatedIndices[k].1 + } else { + left = i < currentList.count ? currentList[i] : nil + } + } else { + left = i < currentList.count ? currentList[i] : nil + } + + let right: T? = j < rightList.count ? rightList[j] : nil + + if let left = left, let right = right { + if left == right { + i += 1 + j += 1 + } else if left < right { + let previousIndex = previousIndices[right.stableId] + insertItems.append((i, right, previousIndex)) + currentList.insert(right, at: i) + if k < updatedIndices.count { + for l in k ..< updatedIndices.count { + updatedIndices[l] = (updatedIndices[l].0 + 1, updatedIndices[l].1, updatedIndices[l].2) + } + } + + i += 1 + j += 1 + } else { + i += 1 + } + } else if let _ = left { + i += 1 + } else if let right = right { + let previousIndex = previousIndices[right.stableId] + insertItems.append((i, right, previousIndex)) + currentList.insert(right, at: i) + + if k < updatedIndices.count { + for l in k ..< updatedIndices.count { + updatedIndices[l] = (updatedIndices[l].0 + 1, updatedIndices[l].1, updatedIndices[l].2) + } + } + + i += 1 + j += 1 + } else { + break + } + } + + for (index, item, _) in updatedIndices { + currentList[index] = item + } + + assert(currentList == rightList, "currentList == rightList") + + return (removeIndices, insertItems, updatedIndices) +} + diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index 88ad453d49..2d897a8e66 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -171,7 +171,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func playOnceWithSound(playAndRecord: Bool) { assert(Queue.mainQueue().isCurrent()) self.player.actionAtEnd = .loopDisablingSound({ [weak self] in - self?.performActionAtEnd() + Queue.mainQueue().async { + self?.performActionAtEnd() + } }) self.player.playOnceWithSound(playAndRecord: playAndRecord) } diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index ced2cb2cc6..8a907be836 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -9,7 +9,7 @@ public func navigateToChatController(navigationController: NavigationController, for controller in navigationController.viewControllers.reversed() { if let controller = controller as? ChatController, controller.chatLocation == chatLocation { if let messageId = messageId { - controller.navigateToMessage(id: messageId, animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index 92be87ef38..4010104b76 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -123,15 +123,15 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { case let .classicHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .none(_, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { arguments.selectSound(.none) }) case let .default(_, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(.default) }) case let .sound(_, _, theme, text, sound, selected): - return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) }) } @@ -267,11 +267,11 @@ public func notificationSoundSelectionController(account: Account, isModal: Bool let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, NotificationSoundSelectionEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { arguments.cancel() }) - let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { arguments.complete() }) diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index cad58d98bd..f52286bb24 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -6,7 +6,7 @@ import TelegramCore import SwiftSignalKit import PassKit -func openChatMessage(account: Account, message: Message, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> ASDisplayNode?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { +func openChatMessage(account: Account, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, transitionNode: @escaping (MessageId, Media) -> ASDisplayNode?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void) -> Bool { var galleryMedia: Media? var otherMedia: Media? var instantPageMedia: [InstantPageGalleryEntry]? @@ -127,19 +127,8 @@ func openChatMessage(account: Account, message: Message, reverseMessageGalleryOr } } }) - /* - NSData *passData = [[NSData alloc] initWithContentsOfFile:[_companion fileUrlForDocumentMedia:documentAttachment].path]; - NSError *error; - PKPass *pass = [[PKPass alloc] initWithData:passData error:&error]; - - if (error == nil) - { - [self presentViewController:[[PKAddPassesViewController alloc] initWithPass:pass] animated:true completion:nil]; - return nil; - } - */ } else { - let gallery = GalleryController(account: account, messageId: message.id, invertItemOrder: reverseMessageGalleryOrder, replaceRootController: { [weak navigationController] controller, ready in + let gallery = GalleryController(account: account, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, replaceRootController: { [weak navigationController] controller, ready in navigationController?.replaceTopController(controller, animated: false, ready: ready) }, baseNavigationController: navigationController) @@ -176,7 +165,7 @@ func openChatMessage(account: Account, message: Message, reverseMessageGalleryOr items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: { dismissAction() - openPeer(peer, .chat(textInputState: nil)) + openPeer(peer, .chat(textInputState: nil, messageId: nil)) })) if let isContact = isContact, !isContact { items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: { diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift new file mode 100644 index 0000000000..dacdb57485 --- /dev/null +++ b/TelegramUI/OpenResolvedUrl.swift @@ -0,0 +1,27 @@ +import Foundation +import TelegramCore +import Postbox +import Display + +func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void) { + switch resolvedUrl { + case let .externalUrl(url): + openExternalUrl(url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController) + case let .peer(peerId): + openPeer(peerId, .chat(textInputState: nil, messageId: nil)) + case let .botStart(peerId, payload): + openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) + case let .groupBotStart(peerId, payload): + break + case let .channelMessage(peerId, messageId): + openPeer(peerId, .chat(textInputState: nil, messageId: messageId)) + case let .stickerPack(name): + present(StickerPackPreviewController(account: account, stickerPack: .name(name)), nil) + case let .instantView(webpage, anchor): + navigationController?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) + case let .join(link): + present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in + openPeer(peerId, .chat(textInputState: nil, messageId: nil)) + }), nil) + } +} diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 279b98917d..e132304ded 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -45,16 +45,20 @@ func openExternalUrl(url: String, presentationData: PresentationData, applicatio } } - if #available(iOSApplicationExtension 9.0, *) { - if let window = navigationController?.view.window { - let controller = SFSafariViewController(url: parsedUrl) - if #available(iOSApplicationExtension 10.0, *) { - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor - controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + if #available(iOSApplicationExtension 9.0, *) { + if let window = navigationController?.view.window { + let controller = SFSafariViewController(url: parsedUrl) + if #available(iOSApplicationExtension 10.0, *) { + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + } + window.rootViewController?.present(controller, animated: true) + } else { + applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) } - window.rootViewController?.present(controller, animated: true) } else { - applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) + applicationContext.applicationBindings.openUrl(url) } } else { applicationContext.applicationBindings.openUrl(url) diff --git a/TelegramUI/OverlayPlayerController.swift b/TelegramUI/OverlayPlayerController.swift index 6ff435d47f..eb3b3eeefc 100644 --- a/TelegramUI/OverlayPlayerController.swift +++ b/TelegramUI/OverlayPlayerController.swift @@ -30,6 +30,8 @@ final class OverlayPlayerController: ViewController { super.init(navigationBarTheme: nil) self.statusBar.statusBarStyle = .Ignore + + self.ready.set(.never()) } required init(coder aDecoder: NSCoder) { @@ -58,6 +60,8 @@ final class OverlayPlayerController: ViewController { } }) + self.ready.set(self.controllerNode.ready.get()) + self.displayNodeDidLoad() } diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 2964918f2c..98c67fd42b 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -6,6 +6,8 @@ import Postbox import TelegramCore final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRecognizerDelegate { + let ready = Promise() + private let account: Account private let peerId: PeerId private let presentationData: PresentationData @@ -46,9 +48,9 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } var openMessageImpl: ((MessageId) -> Bool)? - self.controllerInteraction = ChatControllerInteraction(openMessage: { id in + self.controllerInteraction = ChatControllerInteraction(openMessage: { message in if let openMessageImpl = openMessageImpl { - return openMessageImpl(id) + return openMessageImpl(message.id) } else { return false } @@ -56,6 +58,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false + }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) self.dimNode = ASDisplayNode() @@ -144,10 +147,14 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec openMessageImpl = { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) { - return openChatMessage(account: strongSelf.account, message: message, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _ in }, sendSticker: { _ in }, setupTemporaryHiddenMedia: { _, _, _ in }) + return openChatMessage(account: strongSelf.account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _ in }, sendSticker: { _ in }, setupTemporaryHiddenMedia: { _, _, _ in }) } return false } + + self.ready.set(self.historyNode.historyState.get() |> map { _ -> Bool in + return true + } |> take(1)) } deinit { diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 1229afec10..516765a031 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -60,13 +60,13 @@ public class PeerMediaCollectionController: TelegramController { } } - let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in - if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(id) { + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in + if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id) { guard let navigationController = strongSelf.navigationController as? NavigationController else { return false } strongSelf.mediaCollectionDisplayNode.view.endEditing(true) - return openChatMessage(account: account, message: galleryMessage.message, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { + return openChatMessage(account: account, message: galleryMessage.message, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { self?.mediaCollectionDisplayNode.view.endEditing(true) }, present: { c, a in self?.present(c, in: .window(.root), with: a) @@ -102,9 +102,9 @@ public class PeerMediaCollectionController: TelegramController { } } }, openPeerMention: { _ in - }, openMessageContextMenu: { [weak self] id, _, _ in + }, openMessageContextMenu: { [weak self] message, _, _ in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(id)?.message { + if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in @@ -165,8 +165,8 @@ public class PeerMediaCollectionController: TelegramController { }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in - }, openInstantPage: { [weak self] messageId in - if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(messageId)?.message { + }, openInstantPage: { [weak self] message in + if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController) } }, openHashtag: { _, _ in @@ -215,6 +215,7 @@ public class PeerMediaCollectionController: TelegramController { }, setupReply: { _ in }, canSetupReply: { return false + }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) self.controllerInteraction = controllerInteraction @@ -225,8 +226,8 @@ public class PeerMediaCollectionController: TelegramController { }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - strongSelf.messageContextDisposable.set((combineLatest(chatDeleteMessagesOptions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { options, peer in - if let strongSelf = self, let peer = peer, !options.isEmpty { + strongSelf.messageContextDisposable.set((combineLatest(chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { actions, peer in + if let strongSelf = self, let peer = peer, !actions.options.isEmpty { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] var personalPeerName: String? @@ -237,7 +238,7 @@ public class PeerMediaCollectionController: TelegramController { isChannel = true } - if options.contains(.globally) { + if actions.options.contains(.deleteGlobally) { let globalTitle: String if isChannel { globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe @@ -254,7 +255,7 @@ public class PeerMediaCollectionController: TelegramController { } })) } - if options.contains(.locally) { + if actions.options.contains(.deleteLocally) { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { @@ -367,6 +368,7 @@ public class PeerMediaCollectionController: TelegramController { }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in }, navigateFeed: { + }, openGrouping: { }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index b236800d80..fe1b3c05e1 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -188,7 +188,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { requestOpenPeerFromSearch(peer) } }, openRecentPeerOptions: { _ in - },openMessage: { [weak self] peer, messageId in + }, openMessage: { [weak self] peer, messageId in if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { requestOpenMessageFromSearch(peer, messageId) } diff --git a/TelegramUI/PerformanceSpinner.swift b/TelegramUI/PerformanceSpinner.swift deleted file mode 100644 index 5572752251..0000000000 --- a/TelegramUI/PerformanceSpinner.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit - -private final class SpinnerThread: NSObject { - private var thread: Thread? - private let condition: NSCondition - private var workValue: CGFloat = 0 - - override init() { - self.condition = NSCondition() - - super.init() - - let thread = Thread(target: self, selector: #selector(self.entryPoint), object: nil) - thread.name = "Spinner" - self.thread = thread - thread.start() - } - - @objc func entryPoint() { - while true { - workValue = workValue + CGFloat(sin(Double(workValue))) - usleep(100) - } - } - - func aquire() -> Int { - return 0 - } -} - -//private let atomicSpinner = SpinnerThread() - -func performanceSpinnerAcquire() -> Int { - //return atomicSpinner.aquire() - return 0 -} - -func performanceSpinnerRelease(_ index: Int) -> Void { -} diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 369f74b03c..25b0209000 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -359,15 +359,12 @@ private func ==(lhs: Tail, rhs: Tail) -> Bool { } } -private var cachedCorners: [Corner: DrawingContext] = [:] -private let cachedCornersLock = SwiftSignalKit.Lock() -private var cachedTails: [Tail: DrawingContext] = [:] -private let cachedTailsLock = SwiftSignalKit.Lock() +private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:]) +private var cachedTails = Atomic<[Tail: DrawingContext]>(value: [:]) private func cornerContext(_ corner: Corner) -> DrawingContext { - var cached: DrawingContext? - cachedCornersLock.locked { - cached = cachedCorners[corner] + var cached: DrawingContext? = cachedCorners.with { + return $0[corner] } if let cached = cached { @@ -392,17 +389,19 @@ private func cornerContext(_ corner: Corner) -> DrawingContext { c.fillEllipse(in: rect) } - cachedCornersLock.locked { - cachedCorners[corner] = context + let _ = cachedCorners.modify { current in + var current = current + current[corner] = context + return current } + return context } } private func tailContext(_ tail: Tail) -> DrawingContext { - var cached: DrawingContext? - cachedTailsLock.locked { - cached = cachedTails[tail] + var cached: DrawingContext? = cachedTails.with { + return $0[tail] } if let cached = cached { @@ -459,8 +458,10 @@ private func tailContext(_ tail: Tail) -> DrawingContext { c.fillEllipse(in: rect) } - cachedCornersLock.locked { - cachedTails[tail] = context + let _ = cachedTails.modify { current in + var current = current + current[tail] = context + return current } return context } @@ -1188,30 +1189,36 @@ func mediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { - let blurSourceImage = thumbnailImage ?? fullSizeImage - - if let fullSizeImage = blurSourceImage { - let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withFlippedContext { c in - c.interpolationQuality = .none - c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - - if let blurredImage = thumbnailContext.generateImage() { - let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) - c.interpolationQuality = .medium - c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) - c.setBlendMode(.normal) - c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) + switch arguments.resizeMode { + case .blurBackground: + let blurSourceImage = thumbnailImage ?? fullSizeImage + + if let fullSizeImage = blurSourceImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.interpolationQuality = .medium + c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { + c.fill(arguments.drawingRect) + } + case let .fill(color): + c.setFillColor(color.cgColor) c.fill(arguments.drawingRect) - c.setBlendMode(.copy) - } - } else { - c.fill(arguments.drawingRect) } } diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 85c8073953..04e1403a02 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -71,8 +71,8 @@ private func currentTimeFormat() -> PresentationTimeFormat { } } -public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings), NoError> { - return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings) in +public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings), NoError> { + return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings) in let themeSettings: PresentationThemeSettings if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current @@ -108,8 +108,15 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres callListSettings = CallListSettings.defaultSettings } - return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings) - } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings) in + let inAppNotificationSettings: InAppNotificationSettings + if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings { + inAppNotificationSettings = value + } else { + inAppNotificationSettings = InAppNotificationSettings.defaultSettings + } + + return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) + } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings) in let themeValue: PresentationTheme switch themeSettings.theme { case let .builtin(reference): @@ -131,7 +138,7 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres stringsValue = defaultPresentationStrings } let timeFormat: PresentationTimeFormat = currentTimeFormat() - return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings) + return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) } } diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index b30f1c2976..d3ddead83d 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -146,6 +146,7 @@ enum PresentationResourceKey: Int32 { case chatTitlePanelUnmuteImage case chatTitlePanelCallImage case chatTitlePanelReportImage + case chatTitlePanelGroupingImage case chatHistoryNavigationButtonImage case chatHistoryMentionsButtonImage @@ -186,4 +187,5 @@ enum PresentationResourceKey: Int32 { case genericSearchBar case inAppNotificationBackground + case inAppNotificationSecretChatIcon } diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 2fcc5311eb..430dc1ae25 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -708,6 +708,18 @@ struct PresentationResourcesChat { }) } + static func chatTitlePanelGroupingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelGroupingImage.rawValue, { theme in + return generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/RevealActionGroupIcon"), color: theme.chat.inputPanel.panelControlAccentColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - 3.0), size: image.size)) + } + }) + }) + } + static func chatMessageAttachedContentButtonIncoming(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIncoming.rawValue, { theme in return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.bubble.incomingAccentControlColor, strokeWidth: 1.0, backgroundColor: nil) diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index afcdf2c6f0..1af9d7464a 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -170,4 +170,10 @@ struct PresentationResourcesRootController { })?.stretchableImage(withLeftCapWidth: 8 + 15, topCapHeight: 8 + 15) }) } + + static func inAppNotificationSecretChatIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.inAppNotificationSecretChatIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Notification/SecretLock"), color: theme.inAppNotification.primaryTextColor) + }) + } } diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index da786aaea3..82434e04b2 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -580,7 +580,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign var rightNavigationButton: ItemListNavigationButton? if privacySettings == nil || state.updatingAccountTimeoutValue != nil { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) diff --git a/TelegramUI/ProxySettingsController.swift b/TelegramUI/ProxySettingsController.swift index 5d243180d8..f70450e104 100644 --- a/TelegramUI/ProxySettingsController.swift +++ b/TelegramUI/ProxySettingsController.swift @@ -159,7 +159,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { func item(_ arguments: ProxySettingsControllerArguments) -> ListViewItem { switch self { case let .modeDisabled(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { current in var state = current state.enabled = false @@ -167,7 +167,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }) case let .modeSocks5(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { current in var state = current state.enabled = true @@ -317,13 +317,13 @@ func proxySettingsController(account: Account, currentSettings: ProxySettings?) UIPasteboard.general.string = result - presentImpl?(standardTextAlertController(title: nil, text: presentationData.strings.Username_LinkCopied, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Username_LinkCopied, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, ProxySettingsEntry.ItemGenerationArguments)) in - let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: !state.enabled || state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.enabled || state.isComplete, action: { var proxySettings: ProxySettings? if state.enabled && state.isComplete, let port = Int32(state.port) { proxySettings = ProxySettings(host: state.host, port: port, username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password, useForCalls: state.useForCalls) diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index cb1181f5a2..83a9ec864f 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -317,7 +317,7 @@ public func recentSessionsController(account: Account) -> ViewController { return .complete() } - removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in + removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then((applySessions |> mapError { _ in TerminateAccountSessionError.generic })) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } @@ -382,15 +382,15 @@ public func recentSessionsController(account: Account) -> ViewController { var rightNavigationButton: ItemListNavigationButton? if let sessions = sessions, sessions.count > 1 { if state.terminatingOtherSessions { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } diff --git a/TelegramUI/SearchDisplayControllerContentNode.swift b/TelegramUI/SearchDisplayControllerContentNode.swift index 9a9a43c468..09282e3b2e 100644 --- a/TelegramUI/SearchDisplayControllerContentNode.swift +++ b/TelegramUI/SearchDisplayControllerContentNode.swift @@ -9,8 +9,6 @@ class SearchDisplayControllerContentNode: ASDisplayNode { override init() { super.init() - - self.backgroundColor = UIColor.white } func searchTextUpdated(text: String) { diff --git a/TelegramUI/SelectivePrivacySettingsController.swift b/TelegramUI/SelectivePrivacySettingsController.swift index fe31b38348..3a0906051f 100644 --- a/TelegramUI/SelectivePrivacySettingsController.swift +++ b/TelegramUI/SelectivePrivacySettingsController.swift @@ -159,15 +159,15 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case let .settingHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .everybody(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.everybody) }) case let .contacts(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.contacts) }) case let .nobody(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.nobody) }) case let .settingInfo(theme, text): @@ -363,15 +363,15 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, SelectivePrivacySettingsEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let rightNavigationButton: ItemListNavigationButton if state.saving { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { var wasSaving = false var settings: SelectivePrivacySettings? updateState { state in diff --git a/TelegramUI/SelectivePrivacySettingsPeersController.swift b/TelegramUI/SelectivePrivacySettingsPeersController.swift index d8f4f393e0..3897a75bc4 100644 --- a/TelegramUI/SelectivePrivacySettingsPeersController.swift +++ b/TelegramUI/SelectivePrivacySettingsPeersController.swift @@ -283,13 +283,13 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini var rightNavigationButton: ItemListNavigationButton? if !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index ea7233783d..a04af4c980 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -366,6 +366,9 @@ public func settingsController(account: Account, accountManager: AccountManager) let actionsDisposable = DisposableSet() + let cachedDataDisposable = MetaDisposable() + actionsDisposable.add(cachedDataDisposable) + let updateAvatarDisposable = MetaDisposable() actionsDisposable.add(updateAvatarDisposable) @@ -478,8 +481,6 @@ public func settingsController(account: Account, accountManager: AccountManager) presentControllerImpl?(legacyController, nil) - let theme = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme - var hasPhotos = false if let peer = peer, !peer.profileImageRepresentations.isEmpty { hasPhotos = true @@ -547,7 +548,7 @@ public func settingsController(account: Account, accountManager: AccountManager) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView) |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) - let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { if let peer = peer as? TelegramUser, let cachedData = view.cachedData as? CachedUserData { arguments.openEditing() } diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 28ddba4b09..2195d01e1d 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -120,7 +120,7 @@ private enum StorageUsageEntry: ItemListNodeEntry { case let .peersHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .peer(_, theme, strings, peer, value): - return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { arguments.openPeerMedia(peer.id) }, setPeerIdWithRevealedOptions: { previousId, id in diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index b5c35a56f8..d160756b2f 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -1,7 +1,45 @@ import Foundation import TelegramCore -func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, boldFont: UIFont, fixedFont: UIFont) -> NSAttributedString { +func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString { + var nsString: NSString? + let string = NSMutableAttributedString(string: text) + var skipEntity = false + let stringLength = string.length + for i in 0 ..< entities.count { + if skipEntity { + skipEntity = false + continue + } + let entity = entities[i] + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if nsString == nil { + nsString = text as NSString + } + if range.location + range.length > stringLength { + range.location = max(0, stringLength - range.length) + range.length = stringLength - range.location + } + switch entity.type { + case .Url, .Email, .PhoneNumber, .TextUrl, .Mention, .Hashtag, .BotCommand: + break + case .Bold: + string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range) + case .Italic: + string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range) + case let .TextMention(peerId): + string.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: peerId), range: range) + case .Code, .Pre: + string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range) + default: + break + } + } + return string +} + + +func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, fixedFont: UIFont) -> NSAttributedString { var nsString: NSString? let string = NSMutableAttributedString(string: text, attributes: [NSAttributedStringKey.font: baseFont, NSAttributedStringKey.foregroundColor: baseColor]) var skipEntity = false @@ -48,14 +86,22 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: url, range: range) case .Bold: string.addAttribute(NSAttributedStringKey.font, value: boldFont, range: range) + case .Italic: + string.addAttribute(NSAttributedStringKey.font, value: italicFont, range: range) case .Mention: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkFont !== baseFont { + string.addAttribute(NSAttributedStringKey.font, value: linkFont, range: range) + } if nsString == nil { nsString = text as NSString } string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute), value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) + if linkFont !== baseFont { + string.addAttribute(NSAttributedStringKey.font, value: linkFont, range: range) + } let mention = nsString!.substring(with: range) string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute), value: TelegramPeerMention(peerId: peerId, mention: mention), range: range) case .Hashtag: diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 57d65ef2bd..ef8c762a54 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -5,20 +5,24 @@ import Postbox import TelegramCore public final class TelegramApplicationBindings { + public let isMainApp: Bool public let openUrl: (String) -> Void public let canOpenUrl: (String) -> Bool public let getTopWindow: () -> UIWindow? public let displayNotification: (String) -> Void public let applicationInForeground: Signal public let applicationIsActive: Signal + public let clearMessageNotifications: ([MessageId]) -> Void - public init(openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal) { + public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void) { + self.isMainApp = isMainApp self.openUrl = openUrl self.canOpenUrl = canOpenUrl self.getTopWindow = getTopWindow self.displayNotification = displayNotification self.applicationInForeground = applicationInForeground self.applicationIsActive = applicationIsActive + self.clearMessageNotifications = clearMessageNotifications } } @@ -30,7 +34,7 @@ public final class TelegramApplicationContext { public let mediaManager: MediaManager - let locationManager: DeviceLocationManager + let locationManager: DeviceLocationManager? public let liveLocationManager: LiveLocationManager? public let contactsManager = DeviceContactsManager() @@ -41,6 +45,9 @@ public final class TelegramApplicationContext { return self._presentationData.get() } + public let currentInAppNotificationSettings: Atomic + private var inAppNotificationSettingsDisposable: Disposable? + public let currentAutomaticMediaDownloadSettings: Atomic private let _automaticMediaDownloadSettings = Promise() public var automaticMediaDownloadSettings: Signal { @@ -53,11 +60,16 @@ public final class TelegramApplicationContext { public var navigateToCurrentCall: (() -> Void)? public var hasOngoingCall: Signal? - public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, currentInAppNotificationSettings: InAppNotificationSettings, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground) - self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) - if let stateManager = stateManager, let accountPeerId = accountPeerId, let viewTracker = viewTracker { - self.liveLocationManager = LiveLocationManager(postbox: postbox, network: network, accountPeerId: accountPeerId, viewTracker: viewTracker, stateManager: stateManager, locationManager: self.locationManager, inForeground: applicationBindings.applicationInForeground) + + if applicationBindings.isMainApp { + self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) + } else { + self.locationManager = nil + } + if let stateManager = stateManager, let accountPeerId = accountPeerId, let viewTracker = viewTracker, let locationManager = self.locationManager { + self.liveLocationManager = LiveLocationManager(postbox: postbox, network: network, accountPeerId: accountPeerId, viewTracker: viewTracker, stateManager: stateManager, locationManager: locationManager, inForeground: applicationBindings.applicationInForeground) } else { self.liveLocationManager = nil } @@ -68,6 +80,19 @@ public final class TelegramApplicationContext { self.currentAutomaticMediaDownloadSettings = Atomic(value: currentMediaDownloadSettings) self._presentationData.set(.single(currentPresentationData) |> then(presentationData)) self._automaticMediaDownloadSettings.set(.single(currentMediaDownloadSettings) |> then(automaticMediaDownloadSettings)) + self.currentInAppNotificationSettings = Atomic(value: currentInAppNotificationSettings) + + + let inAppPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings])) + inAppNotificationSettingsDisposable = (postbox.combinedView(keys: [inAppPreferencesKey]) |> deliverOnMainQueue).start(next: { [weak self] views in + if let strongSelf = self { + if let view = views.views[inAppPreferencesKey] as? PreferencesView { + if let settings = view.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { + let _ = strongSelf.currentInAppNotificationSettings.swap(settings) + } + } + } + }) self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in if let strongSelf = self { @@ -94,6 +119,7 @@ public final class TelegramApplicationContext { deinit { self.presentationDataDisposable.dispose() self.automaticMediaDownloadSettingsDisposable.dispose() + self.inAppNotificationSettingsDisposable?.dispose() } public func attachOverlayMediaController(_ controller: OverlayMediaController) { diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index 4dbb944429..f539279bfb 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -16,7 +16,7 @@ private func presentLiveLocationController(account: Account, peerId: PeerId, con return modifier.getMessage(id) } |> deliverOnMainQueue).start(next: { [weak controller] message in if let message = message, let strongController = controller { - let _ = openChatMessage(account: account, message: message, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, dismissInput: { + let _ = openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, dismissInput: { controller?.view.endEditing(true) }, present: { c, a in controller?.present(c, in: .window(.root), with: a) diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 0e217a4b25..c9ef0c14c6 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -14,10 +14,6 @@ private var legacyLocalization = TGLocalization(version: 0, code: "en", dict: [: func updateLegacyLocalization(strings: PresentationStrings) { legacyLocalization = TGLocalization(version: 0, code: strings.languageCode, dict: strings.dict, isActive: true) - - let languages: [String] = [strings.languageCode] - UserDefaults.standard.set(languages, forKey: "AppleLanguages") - UserDefaults.standard.synchronize() } public func updateLegacyComponentsAccount(_ account: Account?) { diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index da42adeb8a..5be5358952 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -91,6 +91,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false + }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) super.init(layerBacked: false, dynamicBounce: false) diff --git a/TelegramUI/ThemeSettingsController.swift b/TelegramUI/ThemeSettingsController.swift index 9b57c58117..98699575c4 100644 --- a/TelegramUI/ThemeSettingsController.swift +++ b/TelegramUI/ThemeSettingsController.swift @@ -133,7 +133,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case let .themeListHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .themeItem(theme, title, value, index): - return ItemListCheckboxItem(theme: theme, title: title, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectTheme(index) }) } diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift index c9c92a2578..3fe26c8998 100644 --- a/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -38,12 +38,25 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me context.setBlendMode(.copy) context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) }), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) { + let imageDimensions = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + 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)])) + var attributes = file.attributes + loop: for i in 0 ..< attributes.count { + switch attributes[i] { + case .ImageSize: + attributes.remove(at: i) + break loop + default: + break + } + } + attributes.append(.ImageSize(size: imageDimensions)) + subscriber.putNext(file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: scaledImageSize, resource: thumbnailResource)]).withUpdatedAttributes(attributes)) subscriber.putCompletion() } else { subscriber.putNext(file.withUpdatedSize(data.size)) diff --git a/TelegramUI/TwoStepVerificationPasswordEntryController.swift b/TelegramUI/TwoStepVerificationPasswordEntryController.swift index ba33e7b65d..5a379bf87d 100644 --- a/TelegramUI/TwoStepVerificationPasswordEntryController.swift +++ b/TelegramUI/TwoStepVerificationPasswordEntryController.swift @@ -337,6 +337,7 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV updateState { $0.withUpdatedUpdating(false) } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let alertText: String switch error { case .generic: @@ -344,7 +345,7 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV case .invalidEmail: alertText = "Please enter valid e-mail address." } - presentControllerImpl?(standardTextAlertController(title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) case let .setupEmail(password): updatePasswordDisposable.set((updateTwoStepVerificationEmail(account: account, currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { update in @@ -361,6 +362,7 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV updateState { $0.withUpdatedUpdating(false) } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let alertText: String switch error { case .generic: @@ -368,11 +370,12 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV case .invalidEmail: alertText = "Please enter valid e-mail address." } - presentControllerImpl?(standardTextAlertController(title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } } else if invalidReentry { - presentControllerImpl?(standardTextAlertController(title: nil, text: "Passwords don't match.\nPlease try again.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Passwords don't match.\nPlease try again.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } @@ -387,13 +390,13 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationPasswordEntryEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.updating { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { var nextEnabled = true switch state.stage { @@ -408,7 +411,7 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV case .hint, .email: break } - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { checkPassword() }) } diff --git a/TelegramUI/TwoStepVerificationResetController.swift b/TelegramUI/TwoStepVerificationResetController.swift index e447527a73..33ad0daf72 100644 --- a/TelegramUI/TwoStepVerificationResetController.swift +++ b/TelegramUI/TwoStepVerificationResetController.swift @@ -161,6 +161,7 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, updateState { return $0.withUpdatedChecking(false) } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let alertText: String switch error { case .generic: @@ -172,7 +173,7 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, case .limitExceeded: alertText = "You have entered invalid code too many times. Please try again later." } - presentControllerImpl?(standardTextAlertController(title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, completed: { updateState { return $0.withUpdatedChecking(false) @@ -189,25 +190,26 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, }, next: { checkCode() }, openEmailInaccessible: { - presentControllerImpl?(standardTextAlertController(title: nil, text: "Your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationResetEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { var nextEnabled = true if state.codeText.isEmpty { nextEnabled = false } - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { checkCode() }) } diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index 2e8baa401d..4b52c8bc7d 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -311,6 +311,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep switch data { case let .access(configuration): if let configuration = configuration { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } switch configuration { case let .set(_, hasRecoveryEmail, _): if hasRecoveryEmail { @@ -332,10 +333,10 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep updateState { $0.withUpdatedChecking(false) } - presentControllerImpl?(standardTextAlertController(title: nil, text: "An error occured. Please try again later.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "An error occured. Please try again later.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } else { - presentControllerImpl?(standardTextAlertController(title: nil, text: "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } case .notSet: break @@ -382,7 +383,8 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep } })) }, openDisablePassword: { - presentControllerImpl?(standardTextAlertController(title: nil, text: "Are you sure you want to disable your password?", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Are you sure you want to disable your password?", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: { var disablePassword = false updateState { state in if state.checking { @@ -463,13 +465,13 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep title = presentationData.strings.TwoStepAuth_Title if let configuration = configuration { if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { switch configuration { case .notSet: break case .set: - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: true, action: { var wasChecking = false var password: String? updateState { state in @@ -490,6 +492,8 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep $0.withUpdatedChecking(false) } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let text: String switch error { case .limitExceeded: @@ -500,7 +504,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep text = "An error occured. Please try again later." } - presentControllerImpl?(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } }) @@ -512,7 +516,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep case .manage: title = presentationData.strings.PrivacySettings_TwoStepAuth if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } } diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index f728d6ce95..820e60f6aa 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -639,7 +639,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message): - let gallery = GalleryController(account: account, messageId: message.id, replaceRootController: { controller, ready in + let gallery = GalleryController(account: account, source: .peerMessagesAtId(message.id), replaceRootController: { controller, ready in if let baseNavigationController = baseNavigationController { baseNavigationController.replaceTopController(controller, animated: false, ready: ready) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 11ee840919..910d02ce81 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -276,9 +276,9 @@ private enum UserInfoEntry: ItemListNodeEntry { }, tag: UserInfoEntryTag.phoneNumber) case let .userName(theme, text, value): return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { - arguments.displayUsernameContextMenu("@" + value) + arguments.displayUsernameContextMenu("@\(value)") }, longTapAction: { - arguments.displayCopyContextMenu(.username, value) + arguments.displayCopyContextMenu(.username, "@\(value)") }, tag: UserInfoEntryTag.username) case let .sendMessage(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { @@ -525,7 +525,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) } |> deliverOnMainQueue).start(next: { peer, current in if let peer = peer, let current = current { - presentControllerImpl?(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) })]), nil) } @@ -580,15 +580,16 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll controller?.dismissAnimated() } let notificationAction: (Int32) -> Void = { muteUntil in - let muteState: PeerMuteState + let muteInterval: Int32? if muteUntil <= 0 { - muteState = .unmuted + muteInterval = nil } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) + muteInterval = Int32.max } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + muteInterval = muteUntil } - changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + + changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start()) } var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, action: { @@ -644,7 +645,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } else { text = presentationData.strings.UserInfo_UnblockConfirmation(peer.displayTitle).0 } - presentControllerImpl?(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) })]), nil) }) @@ -729,7 +730,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll var leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton if let editingState = state.editingState { - leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditingState(nil) } @@ -741,9 +742,9 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } if state.savingData { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { var updateName: ItemListAvatarAndNameInfoItemName? updateState { state in if let editingState = state.editingState, let editingName = editingState.editingName { @@ -774,7 +775,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) } } else { - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { if let user = peer { updateState { state in return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user))) @@ -860,7 +861,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, error: { _ in if let controller = controller { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - controller.present(standardTextAlertController(title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift index 3d2686f3f3..10a651157c 100644 --- a/TelegramUI/UsernameSetupController.swift +++ b/TelegramUI/UsernameSetupController.swift @@ -245,7 +245,7 @@ public func usernameSetupController(account: Account) -> ViewController { } } - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in if state.editingPublicLinkText != peer.addressName { @@ -278,7 +278,7 @@ public func usernameSetupController(account: Account) -> ViewController { }) } - let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) diff --git a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index ff85525c8f..53c88ca11a 100644 --- a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -124,6 +124,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor self.listView.limitHitTestToNodes = true self.listView.isHidden = true + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true super.init(account: account, theme: theme, strings: strings) diff --git a/TelegramUI/VoiceCallDataSavingController.swift b/TelegramUI/VoiceCallDataSavingController.swift index 71a1e3f75e..49476e5812 100644 --- a/TelegramUI/VoiceCallDataSavingController.swift +++ b/TelegramUI/VoiceCallDataSavingController.swift @@ -75,15 +75,15 @@ private enum VoiceCallDataSavingEntry: ItemListNodeEntry { func item(_ arguments: VoiceCallDataSavingControllerArguments) -> ListViewItem { switch self { case let .never(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.never) }) case let .cellular(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.cellular) }) case let .always(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.always) }) case let .info(theme, text): diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift index 44211f7943..4d98e0f79e 100644 --- a/TelegramUI/WebEmbedVideoContent.swift +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -8,6 +8,13 @@ import TelegramCore import LegacyComponents func webEmbedVideoContentSupportsWebpage(_ webpageContent: TelegramMediaWebpageLoadedContent) -> Bool { + switch websiteType(of: webpageContent) { + case .instagram: + return true + default: + break + } + let converted = TGWebPageMediaAttachment() converted.url = webpageContent.url diff --git a/submodules/libtgvoip b/submodules/libtgvoip deleted file mode 160000 index 6fff5b379d..0000000000 --- a/submodules/libtgvoip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6fff5b379d66cbb637d497c2f3a115e2487ec25f