From cbbcb12b3b01eb2493d07165ecb87b6091e39cce Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 15 Aug 2017 14:44:14 +0300 Subject: [PATCH] no message --- .../MentionBadgeIcon.imageset/Contents.json | 22 + .../ic_mention2@2x.png | Bin 0 -> 819 bytes .../ic_mention2@3x.png | Bin 0 -> 1321 bytes .../Contents.json | 0 .../StarIconEmpty.imageset/Contents.json | 22 + .../ic_favesticker@2x.png | Bin 0 -> 874 bytes .../ic_favesticker@3x.png | Bin 0 -> 1335 bytes .../StarIconFilled.imageset/Contents.json | 22 + .../ic_favedsticker@2x.png | Bin 0 -> 699 bytes .../ic_favedsticker@3x.png | Bin 0 -> 911 bytes .../Chat/Input/Media/Contents.json | 9 + .../Contents.json | 22 + .../ic_favestickerstab@2x.png | Bin 0 -> 2181 bytes .../ic_favestickerstab@3x.png | Bin 0 -> 3482 bytes .../NavigateToMentions.imageset/Contents.json | 22 + .../ic_mention@2x.png | Bin 0 -> 2215 bytes .../ic_mention@3x.png | Bin 0 -> 3263 bytes .../Chat/Title Panels/Contents.json | 9 + .../InfoIcon.imageset/Contents.json | 22 + .../InfoIcon.imageset/PanelInfoIcon@2x.png | Bin 0 -> 706 bytes .../InfoIcon.imageset/PanelInfoIcon@3x.png | Bin 0 -> 1116 bytes .../ReportIcon.imageset/Contents.json | 22 + .../PanelReportIcon@2x.png | Bin 0 -> 845 bytes .../PanelReportIcon@3x.png | Bin 0 -> 1242 bytes .../SearchIcon.imageset/Contents.json | 22 + .../PanelSearchIcon@2x.png | Bin 0 -> 654 bytes .../PanelSearchIcon@3x.png | Bin 0 -> 909 bytes .../BackArrow.imageset}/Contents.json | 2 +- .../InstantPageBackArrow@2x.png | Bin 0 -> 306 bytes Images.xcassets/Instant View/Contents.json | 9 + .../SettingsIcon.imageset/Contents.json | 22 + .../InstantViewSettingsIcon@2x.png | Bin 0 -> 829 bytes .../InstantViewSettingsIcon@3x.png | Bin 0 -> 1133 bytes TelegramUI.xcodeproj/project.pbxproj | 108 +++- .../xcschemes/xcschememanagement.plist | 2 +- TelegramUI/ActivityIndicator.swift | 59 +- TelegramUI/AudioWaveformNode.swift | 2 +- ...ationSequenceCodeEntryControllerNode.swift | 6 +- ...quenceCountrySelectionControllerNode.swift | 6 +- ...nSequencePasswordEntryControllerNode.swift | 6 +- ...tionSequencePhoneEntryControllerNode.swift | 6 +- ...rizationSequenceSignUpControllerNode.swift | 6 +- ...rizationSequenceSplashControllerNode.swift | 6 +- .../AutomaticMediaDownloadSettings.swift | 50 ++ TelegramUI/AvatarNode.swift | 2 +- .../BotCheckoutInfoControllerNode.swift | 6 +- ...heckoutNativeCardEntryControllerNode.swift | 6 +- .../BotCheckoutWebInteractionController.swift | 8 + ...CheckoutWebInteractionControllerNode.swift | 6 + TelegramUI/CallControllerKeyPreviewNode.swift | 4 +- TelegramUI/CallControllerNode.swift | 6 +- TelegramUI/CallListControllerNode.swift | 6 +- .../ChangePhoneNumberControllerNode.swift | 6 +- .../ChangePhoneNumberIntroController.swift | 6 +- TelegramUI/ChannelInfoController.swift | 23 +- .../ChannelMembersSearchControllerNode.swift | 6 +- .../ChatChannelSubscriberInputPanelNode.swift | 54 +- TelegramUI/ChatController.swift | 243 ++++--- TelegramUI/ChatControllerInteraction.swift | 12 +- TelegramUI/ChatControllerNode.swift | 46 +- TelegramUI/ChatDocumentGalleryItem.swift | 2 + TelegramUI/ChatHistoryGridNode.swift | 16 +- TelegramUI/ChatHistoryListNode.swift | 75 ++- TelegramUI/ChatHistoryLocation.swift | 7 +- .../ChatHistoryNavigationButtonNode.swift | 14 +- TelegramUI/ChatHistoryNavigationButtons.swift | 106 +++ TelegramUI/ChatHistoryViewForLocation.swift | 14 +- TelegramUI/ChatInfoTitlePanelNode.swift | 114 +++- .../ChatInterfaceInputContextPanels.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 141 ++-- .../ChatInterfaceStateContextQueries.swift | 2 +- TelegramUI/ChatListController.swift | 12 + TelegramUI/ChatListControllerNode.swift | 6 +- TelegramUI/ChatListItem.swift | 59 +- TelegramUI/ChatListNode.swift | 8 +- TelegramUI/ChatListNodeEntries.swift | 17 +- TelegramUI/ChatListNodeLocation.swift | 6 +- TelegramUI/ChatListSearchContainerNode.swift | 2 +- TelegramUI/ChatMediaInputGridEntries.swift | 2 +- ...> ChatMediaInputMetaSectionItemNode.swift} | 55 +- TelegramUI/ChatMediaInputNode.swift | 63 +- TelegramUI/ChatMediaInputPanelEntries.swift | 43 +- TelegramUI/ChatMediaInputStickerPane.swift | 2 +- .../ChatMessageAttachedContentNode.swift | 211 +++++- TelegramUI/ChatMessageBubbleContentNode.swift | 3 + TelegramUI/ChatMessageBubbleItemNode.swift | 8 + .../ChatMessageFileBubbleContentNode.swift | 2 +- .../ChatMessageGameBubbleContentNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 53 +- .../ChatMessageInteractiveMediaNode.swift | 85 ++- .../ChatMessageInvoiceBubbleContentNode.swift | 2 +- TelegramUI/ChatMessageItem.swift | 5 +- TelegramUI/ChatMessageItemContent.swift | 16 + TelegramUI/ChatMessageItemView.swift | 6 +- .../ChatMessageMediaBubbleContentNode.swift | 7 +- TelegramUI/ChatMessageNotificationItem.swift | 4 +- TelegramUI/ChatMessageReplyInfoNode.swift | 2 +- ...hatMessageThrottledProcessingManager.swift | 17 +- .../ChatMessageWebpageBubbleContentNode.swift | 33 +- .../ChatPanelInterfaceInteraction.swift | 6 +- .../ChatPresentationInterfaceState.swift | 43 +- TelegramUI/ChatSearchInputPanelNode.swift | 2 +- .../ChatTextInputAudioRecordingTimeNode.swift | 2 +- TelegramUI/ChatTitleView.swift | 4 +- TelegramUI/ComposeControllerNode.swift | 6 +- .../ContactMultiselectionControllerNode.swift | 6 +- .../ContactSelectionControllerNode.swift | 6 +- TelegramUI/ContactsControllerNode.swift | 6 +- .../DataAndStorageSettingsController.swift | 2 +- TelegramUI/EmbedVideoNode.swift | 82 ++- TelegramUI/FFMpegAudioFrameDecoder.swift | 2 +- TelegramUI/FFMpegMediaFrameSource.swift | 4 +- .../FFMpegMediaFrameSourceContext.swift | 26 +- ...pegMediaPassthroughVideoFrameDecoder.swift | 8 +- TelegramUI/FFMpegMediaVideoFrameDecoder.swift | 2 +- TelegramUI/FFMpegPacket.swift | 10 +- TelegramUI/FetchVideoMediaResource.swift | 2 +- TelegramUI/FrameworkBundle.swift | 1 - TelegramUI/GalleryController.swift | 8 +- TelegramUI/GalleryControllerNode.swift | 6 +- TelegramUI/GalleryItemNode.swift | 6 +- TelegramUI/GameController.swift | 8 + TelegramUI/GameControllerNode.swift | 65 +- TelegramUI/GameControllerTitleView.swift | 2 +- TelegramUI/GroupInfoController.swift | 12 +- TelegramUI/HashtagSearchControllerNode.swift | 6 +- ...rizontalStickersChatContextPanelNode.swift | 6 +- TelegramUI/InstantPageController.swift | 10 +- TelegramUI/InstantPageControllerNode.swift | 97 ++- TelegramUI/InstantPageLayout.swift | 606 ++++++------------ TelegramUI/InstantPageNavigationBar.swift | 142 ++++ TelegramUI/InstantPageTextItem.swift | 40 +- TelegramUI/InstantPageTheme.swift | 7 + TelegramUI/InstantPageTileNode.swift | 2 +- TelegramUI/ItemListControllerNode.swift | 6 +- TelegramUI/LanguageSelectionController.swift | 2 +- .../LanguageSelectionControllerNode.swift | 6 +- TelegramUI/LegacyAttachmentMenu.swift | 25 +- TelegramUI/LegacyCamera.swift | 136 +--- TelegramUI/LegacyComponentsStickers.swift | 159 +++++ TelegramUI/LegacyController.swift | 218 +++++-- TelegramUI/LegacyControllerNode.swift | 6 +- TelegramUI/LegacyEmptyController.swift | 48 +- TelegramUI/LegacyImageDownloadActor.swift | 77 +++ TelegramUI/LegacyImageProcessors.h | 13 + TelegramUI/LegacyImageProcessors.m | 56 ++ TelegramUI/LegacyLocationController.swift | 16 +- TelegramUI/LegacyLocationPicker.swift | 11 +- TelegramUI/LegacyMediaLocations.swift | 42 ++ TelegramUI/LegacyMediaPickers.swift | 57 +- TelegramUI/LegacyNavigationController.swift | 2 +- ...egacyPeerAvatarPlaceholderDataSource.swift | 118 ++++ TelegramUI/LegacySuggestionContext.swift | 58 ++ TelegramUI/ManagedAudioPlaylistPlayer.swift | 6 + TelegramUI/MapInputControllerNode.swift | 6 +- ...MediaNavigationAccessoryItemListNode.swift | 4 +- TelegramUI/MediaPlayerTimeTextNode.swift | 2 +- TelegramUI/MediaTrackDecodableFrame.swift | 13 +- TelegramUI/NetworkStatusTitleView.swift | 37 +- .../NotificationContainerControllerNode.swift | 6 +- TelegramUI/OverlayMediaControllerNode.swift | 8 +- TelegramUI/PasscodeOptionsController.swift | 17 +- TelegramUI/PeerMediaAudioPlaylist.swift | 30 +- .../PeerMediaCollectionController.swift | 12 +- .../PeerMediaCollectionControllerNode.swift | 56 +- .../PeerMediaCollectionSectionsNode.swift | 59 ++ TelegramUI/PeerSelectionControllerNode.swift | 6 +- TelegramUI/PhotoResources.swift | 2 +- .../PictureInPictureVideoControlsNode.swift | 2 +- TelegramUI/PresentationData.swift | 17 +- TelegramUI/PresentationResourceKey.swift | 15 + TelegramUI/PresentationResourcesChat.swift | 95 +++ .../PresentationResourcesChatList.swift | 11 +- .../PresentationResourcesRootController.swift | 14 +- TelegramUI/PresentationStrings.swift | 167 +++++ TelegramUI/RadialProgressContentNode.swift | 283 ++++++++ TelegramUI/RadialProgressNode.swift | 8 +- TelegramUI/RadialStatusBackgroundNode.swift | 46 ++ TelegramUI/RadialStatusContentNode.swift | 21 + TelegramUI/RadialStatusIconContentNode.swift | 109 ++++ TelegramUI/RadialStatusNode.swift | 186 ++++++ TelegramUI/RadialTimeoutNode.swift | 4 +- .../Resources/ChatWallpaperBuiltin0.jpg | Bin .../SecretMediaPreviewControllerNode.swift | 6 +- TelegramUI/SettingsController.swift | 22 +- TelegramUI/ShareController.swift | 12 +- TelegramUI/ShareControllerNode.swift | 2 +- TelegramUI/SoftwareVideoSource.swift | 8 +- .../StickerPackPreviewControllerNode.swift | 4 +- TelegramUI/StickerPackPreviewGridItem.swift | 2 +- TelegramUI/StickerPreviewControllerNode.swift | 8 +- TelegramUI/StickerResources.swift | 66 +- TelegramUI/TelegramApplicationContext.swift | 18 +- .../TelegramInitializeLegacyComponents.swift | 266 +++++++- TelegramUI/TelegramVideoNode.swift | 2 +- TelegramUI/TextNode.swift | 2 +- TelegramUI/ThemeGridControllerNode.swift | 12 +- TelegramUI/TransformImageNode.swift | 4 +- submodules/libtgvoip | 2 +- .../LegacyLocationVenueIconDataSource.swift | 127 ++++ 200 files changed, 4890 insertions(+), 1362 deletions(-) create mode 100644 Images.xcassets/Chat List/MentionBadgeIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat List/MentionBadgeIcon.imageset/ic_mention2@2x.png create mode 100644 Images.xcassets/Chat List/MentionBadgeIcon.imageset/ic_mention2@3x.png rename Images.xcassets/Chat/{Wallpapers => Context Menu}/Contents.json (100%) create mode 100644 Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png create mode 100644 Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png create mode 100644 Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png create mode 100644 Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png create mode 100644 Images.xcassets/Chat/Input/Media/Contents.json create mode 100644 Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@2x.png create mode 100644 Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@3x.png create mode 100644 Images.xcassets/Chat/NavigateToMentions.imageset/Contents.json create mode 100644 Images.xcassets/Chat/NavigateToMentions.imageset/ic_mention@2x.png create mode 100644 Images.xcassets/Chat/NavigateToMentions.imageset/ic_mention@3x.png create mode 100644 Images.xcassets/Chat/Title Panels/Contents.json create mode 100644 Images.xcassets/Chat/Title Panels/InfoIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Title Panels/InfoIcon.imageset/PanelInfoIcon@2x.png create mode 100644 Images.xcassets/Chat/Title Panels/InfoIcon.imageset/PanelInfoIcon@3x.png create mode 100644 Images.xcassets/Chat/Title Panels/ReportIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Title Panels/ReportIcon.imageset/PanelReportIcon@2x.png create mode 100644 Images.xcassets/Chat/Title Panels/ReportIcon.imageset/PanelReportIcon@3x.png create mode 100644 Images.xcassets/Chat/Title Panels/SearchIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Title Panels/SearchIcon.imageset/PanelSearchIcon@2x.png create mode 100644 Images.xcassets/Chat/Title Panels/SearchIcon.imageset/PanelSearchIcon@3x.png rename Images.xcassets/{Chat/Wallpapers/Builtin0.imageset => Instant View/BackArrow.imageset}/Contents.json (84%) create mode 100644 Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png create mode 100644 Images.xcassets/Instant View/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json create mode 100644 Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png create mode 100644 Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png create mode 100644 TelegramUI/ChatHistoryNavigationButtons.swift rename TelegramUI/{ChatMediaInputRecentStickerPacksItem.swift => ChatMediaInputMetaSectionItemNode.swift} (59%) create mode 100644 TelegramUI/ChatMessageItemContent.swift create mode 100644 TelegramUI/InstantPageNavigationBar.swift create mode 100644 TelegramUI/InstantPageTheme.swift create mode 100644 TelegramUI/LegacyComponentsStickers.swift create mode 100644 TelegramUI/LegacyImageDownloadActor.swift create mode 100644 TelegramUI/LegacyImageProcessors.h create mode 100644 TelegramUI/LegacyImageProcessors.m create mode 100644 TelegramUI/LegacyMediaLocations.swift create mode 100644 TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift create mode 100644 TelegramUI/LegacySuggestionContext.swift create mode 100644 TelegramUI/PeerMediaCollectionSectionsNode.swift create mode 100644 TelegramUI/RadialProgressContentNode.swift create mode 100644 TelegramUI/RadialStatusBackgroundNode.swift create mode 100644 TelegramUI/RadialStatusContentNode.swift create mode 100644 TelegramUI/RadialStatusIconContentNode.swift create mode 100644 TelegramUI/RadialStatusNode.swift rename Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg => TelegramUI/Resources/ChatWallpaperBuiltin0.jpg (100%) create mode 100644 third-party/RMIntro/LegacyLocationVenueIconDataSource.swift diff --git a/Images.xcassets/Chat List/MentionBadgeIcon.imageset/Contents.json b/Images.xcassets/Chat List/MentionBadgeIcon.imageset/Contents.json new file mode 100644 index 0000000000..31005b33fa --- /dev/null +++ b/Images.xcassets/Chat List/MentionBadgeIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_mention2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_mention2@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/MentionBadgeIcon.imageset/ic_mention2@2x.png b/Images.xcassets/Chat List/MentionBadgeIcon.imageset/ic_mention2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cdad6336ad60b68017aa06721059bc81758e4c35 GIT binary patch literal 819 zcmV-31I+x1P)Px%?MXyIR9Fe^mRo3!aTvhAZ&(gP!(?$oIn-PzD>uemAcu0blALxU&Lvul!no|}=vweO~p6Bg(|NGv3@4iLJp8EBipZCA_@a)TE z#^ry%1KCnJ?0^9<>A^SwwJ;wlp$2xt1MqScr}RV6yblI-Vh4H~OiVm23Gyr93Yc8O z82uq=I&2frtuS?@3x_%CkGTHp*drc1)=#lKbCVkw<*|a44t>ow1bsAB8--=?NE>tL zVJ9cH0-8YnvI}qkDxg}44vc>uMEYP((L+#FFV!l$d;_s^I0Zj7@6`wA)L9>~WiH-q z@kCAWIc<1@mSqzOzP$Y=k1@``W%z7C)oY?A3}S5>Z};(nHigf%xXBj~TMqU`9LJ8f zDE0z)2_|cU-tC%@{^XaLq&)v0TD&nBa!}*41@fW+CtnWy|K~-ez z1?rBfej7FFce3|~f}FS=E*9M#jeT;t$c#qHFPr*OwV&MVoapt(jJ6AW{WY}$eUaeu4xV-TuvSMXb z%d&Z4LDh}ci$4wQ1#Pi-qNeze4jDo(@`d;_;g;q@)l1;L24ADMzT4Rv$^ZT*a8K%B7Hk0fiZCM?z*-#*C}vRHqOIT2n~K%@JoU2qZi3u} zez*gp`;kJ!4Cn+AImBjvk-uVdQTd_a57CGpTg`v`Ai*JU^F443=7kq0ChK@V>22 x#TYKX@8sXDY|3ho?fDj-vgLBPx(<4Ht8RA>e5nQMquRTRg)KGUX*$igzg#0t_LL`gmvii{GhXq3R{K@cVMuqZ;P z59NpY(1Rd~${>RLqS?npCI|%|DfU3b!oo&$#?l`6Aa%y+_mA1b|Loc4%suyBWb-h^Kv zmOpUZp2}^AkK*cF19druq4j@ZnNElw4NpKPxO8JYUZz%6u@t=uj(AV#+dxaBW+DGv z_!L}vFc#HZG)|=0B>35TK>rBF#j&;8)OsE?@E}J~X4YCSlSiSop=#gtlT1K^3~E0e z^`j@$n7YbSpfxJ`Roz7kMnvroqyCl^gcmnaNKcJ#t)RZvON+8-pAIjv3n=uOm7DDfzJ?k#A1{8pW5?2 zz%)zVXL`TN*Q@ob^&MnePztVb%z%5qa{n~_9Mdyu-UrZ)7$n&Vv(!s~-`KgGghE{7 zm}Kp~_@kCr`Ww(*KFCEuNnEv7gXkzZn8c6GobD6iP0v&G+k$eH1wj#^(5{P-&;)1#zmYd|_%U(^cJiqaYSON)@)CF*Hh{Jd@55v8-y@Xkc-RVq$%;_7 z<|{qusOR!>&BZK{f;!Yc2RdWKq6fiJ3old$n^Dc$PY@38dU-A8CAO}xYL$%)R6jl5 z1y_UiTzXTtQ~w0l#t5Cvfs+vA1j>@eR3D-$4EWJ7db_@gp7$agqLG#8hQY41!3R~p z(k(1eW_Uvx>_hNR7`00000NkvXXu0mjf?DK<; literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Wallpapers/Contents.json b/Images.xcassets/Chat/Context Menu/Contents.json similarity index 100% rename from Images.xcassets/Chat/Wallpapers/Contents.json rename to Images.xcassets/Chat/Context Menu/Contents.json diff --git a/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json b/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json new file mode 100644 index 0000000000..eba3e80b71 --- /dev/null +++ b/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_favesticker@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_favesticker@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png b/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1a2b5d103512343e745ac3522d80b075e821770a GIT binary patch literal 874 zcmV-w1C{)VP)Px&BuPX;R9Fe^mQRRHQ5?tLn;DaZKVukwGAUwX#Ked+lrW`47ARq1VZmlfu^Pif z6c!c|X*MhmBVAxSsT}Wb3ea0 z9pqSrW;UCK9&uH(2hF%MN9oW{(?}USG!1Rhirm-0Cli}H^j$=(Koc8HBTu+i*i`Nh zn|Ozf75z#BBjLGeWNWYiViVvC{Dx)0xuV>&=y|B@W&!=L zO@Za0A3-lj&tPiUV#HN*4kYd5c~}p1Fg&crJBGn1*ajWY1q0x!lfc9%Q|6P<0(#s* z>CAiq*FgvSj3}RsIg%^k9=Pfua1)NeqKu*(1M+KOCtL&_M?WAaZNQuyHtA^+XPQbi zv;pd2gjca=5`Gmh9ae&xmRh~aNgNGtRe^SU73GcWaAxVT?rl z7s5x7&crB9r&6>DeySSnk8Kc9fL|l@$r$O^{qK;ED9fIrP{)G(hi=Qxh#$dEJ7-^o zXSvThLu0vOpvV0kq*bwson-2(;iZgFJCr#o`3*5@mLs~KpF_HedYy^VEg0O+1slOk z$HFU+<^|{C+*AAXuR|)eI8L2cF2<*g`F5`YCy82phDmx&CY|ZfVch56V_d32-Es$Q z-RopBnaZ7>NW^Wh#+df|n1|%_nV9tnNy!YwZE%(;@6NRG8?kF|$n1r&JmScY4C@%T z!8%jeWex0=1&6_GXo3Y$ZRecp12eXNtTUbGkOe1$^e$+@coH0g_Yjm0_sh`eEM5r)&?tDFPUZZ$+L#QZoo3Ih;SVIQ$+at$k7g5$6x zsxfzt{94d!PETMT6z(;?scA&Ha|IrTo0e0os7P8Kc6951J07*qoM6N<$f}>lO AmH+?% literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png b/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b8aa993271f1df7e0e955111569766cce7bdd46d GIT binary patch literal 1335 zcmV-71<3k|P)Px(@kvBMRA>e5naz(BRTRY;kYR8jL`eA1XksEt6eUJX*svhTNP-I%lG&NS0%IZu zLRhh2OX9}=fHf-@xG@A=C*cEAdwzhxmTHn5(eaxYzCEtmSh7AoNsGzzW(vxDh^2pqH^USmho zV+Q6J@%vpddL*?xe$)s2<7)rF6zGLg#A46@W`}*;w;6EQ2H_iEv5yZ#8_~yr*-tc{ z%o9#*9B7Q$vAnU5T|bI-BRX!%gD-sKFwWnxHBC99KAT5+T`dHyB**%I7x6EF?O+u! zJ5A&5k&%%(>&WutlAh1O4j2E0_8D*$di&&u5xs0Uok;f zS*EoW!$iGYd;?y~oIT(97j6pp)xN+=Uv4v}bO;R_8N38#XLnrOB&jZ~1~9uqV{P%w zYDY*DkT$ahjkYHKk)f{l;m9`5jT(qGilr1MXLNMB-{n=8Uua?=*Cg0 zi8FBH5|gG~eGx(832-N&ng7t%%!a;@Wh50B5T}{6Ok@m=*9E~gaN8L1v*6`;4B45) z5x8)PNyE16(UE&0+6w+mXy-q4BU=%frkPH;fjG>>?Vr2Nv3+YmxCQ)f3hG?vF!p`1Xu&EnpTI~9zIOQr-L{RX0(F8{?&_4)C!{&_g-lD_KN zv6}wn<%?dD#wz%3FCt3J^&1C0am6ZFuWM$Vy`k8w_TK08TEIpwzRuQJ_M0pk9G`1qN#c9s-C5yQN^Ux8?u<002ovPDHLkV1n;%cS8UG literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json b/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json new file mode 100644 index 0000000000..7d96366e1c --- /dev/null +++ b/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_favedsticker@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_favedsticker@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png b/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..46f6daf33e53d41019042e736914202b314b58a0 GIT binary patch literal 699 zcmV;s0!00ZP)Px%bxA})R9Fe^m(NQTQ5460D&od8(hPblGodoVNN{5yf#S+qC?UFc=|2$E!lla= zZ6c_RL5x7wN_4*u~OV|5m?woneJX*|w&%3|QJ?A^;zIijR zt}eH{0(k{$SAlZ5JPwO62TirhtjL6*VK@b2uN2#Adq6XFtu?e1LV>EEEcdv|fu8h@eaCd}pC_ zIi1ID{3wBYIR12;kEiciY@1ng(4If}GfbJTAYvM#!yWC~8wR%FF9;ue@| zyqH}%V&eoHTS3$68gcIk*r1dX?nVGP?xVDn5yyyoO+V^B2p1DIOQ6{l1xlzD__xa0 zVhPp=yO5R*+VP|M>hkg{e?pL@s6Ywlf8)QZ*`pR11pBTDs_^;21)JYKuX|E~9o}gA zMRlB{0(De>$g`WSqMm+;WzTCs$G5uU*kynyr`a~0Y0 hiM#@N1^%B3`~fWUJVc@po^1dC002ovPDHLkV1lyYL8kx! literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png b/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..16beedb0ec76ecbde0b0d4b04d61b013bf92bc88 GIT binary patch literal 911 zcmV;A191F_P)Px&Nl8RORA>e5n!9ThQ5eN{)g&8|#t4xhiiM99Y7ir&NfFdEX;RpvGeMCwLR$R? z{F7|d#*{%wBhey?A_gC*s3f3Se+Op5GS1H9-aE51>;2#`GxvUvbH2OW+1;5&!<+Ab z?||=s?||>X|J{LhyS)Z>0<86QHxRi3qPEhPqER^vK19_9W5Z>LK2%1Ljv9A4Ki+X2 zo@z{U{bQ_R3%rk6-Ec^At)dl|xx#l4Gm#)jbGH-b>odj54S{#zQYIYbGgPFqvQ}8T z@QDmUOOrR1N|1RZRVil^OusAg^r`Fi?czqF4An zq)!_VSBzroVXThsxetM@DTmX0bJMnk5E>)L*#PG_o(I>!G2p^JljJej0x!WUq1kMH z;h5Vp3r3hkT>+iqbS}ZYFbS6UgC=mkK_fI~ZU~(O>!2?745Ip!#aiMD6R1t?Ik*%X zUERS=s9k2Y_oEck1dkK26Ah}#^*Dk*M!BZlmK5+h8dQU8asCv?Gf^-dY7?{y{=@}W zbWB`V1MdG&OcX3{0v~Z$0>5=#aTCX-fU_M{Hn2so2dZk~wrHm^5>3Jduv*kWg^Way7M2pM38+Bc>P+$)HsDNm3nCn8*?J$LA!S^1DR_$50s#3C1a0YzsfoRp7 zNugKFG=;T$mu<$oapG6YtSv&VzPnz`>Tl_{$$fUpG)L+ekBo_*Wb||4_S7bcE5W-q zByz@D)p8~HOit1^?pC#23BF*XAZM&qE$dB-4NH$keuBd}iJQ9X*U$(X*PclN7b`qT z;j+X^qiU(9k+~3T^${~--Dnus^nwjKZrp8yT49KB%_Ct3i=#>Lx76DRYU;o4lR4{~ z)XZ5T#+@B{6zqen_5#P7;Nb3{^I%MKSxq%ZV<;6nMA54m8S0tuGU&RSYcBASZx)*_ zR$WYXSlvirrkBs|fy@uM@zv`QicxHmU5}wu>=0}wK-}Lw`n~!9jF>V|Y>*@F_qJ;4 zC7AM!!uqI&fA@UjT#q=9x?)1Eu^^{%%T#OGReqy95NLrZP}tja${o~j`n=dkun+Pb l@E!0S@E!0S@EtJfz+Wi6*=2l_dm#V-002ovPDHLkV1f}mu($vK literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Media/Contents.json b/Images.xcassets/Chat/Input/Media/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/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/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json new file mode 100644 index 0000000000..0c077faf04 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_favestickerstab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_favestickerstab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@2x.png b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d30bef49559d316a42c2f5367bd16342d760ff5f GIT binary patch literal 2181 zcmV;02zvL4P)Px-KS@MERA>e5T5D`nMHHTy-K9!Q6fi`Cs6^T*qC(IF^pE|a(HJGZs$fugbQh`8 z2gG(4(D>-K_~=qzvb%_{2nLJ~5~7LG`a=yrFd9*eEh35}nh2FBg|d6^_?^3Zws-4p zyLVsuV{g))nKS40otZiJ-nlD8<*H<$l7UJFDjE3yWymHGw}@|43=1vlRRpd zL(bG6mlp1ao;}1yi(D!%f|& z;BYuRR>Z|N1e>rg#h#HKEUYoeB*7`twQLj{Z7QH58eY3TI{Vm3@fr5x95O4V7<6gb zI;sohY9m>aBVDpVwno)+D3I5KM^8_Wd+&iCFO_G^YE$ZJVajSDbq(O5Na>60F(@S) z{7v4?c3!r2dnno-zS+|*J6u~^8y_Y+YIjt?o!#M! zO;g+kOEp@EYN;)7Q!opjk*ve;DdAELfyVk*Mv@yo7VM5L6o$D8zRns{jF(lz9 zEp;D!^W0x3OyHzoxuDYsEI(gw>5ho9D1Nh*UAdGqbsu16#Auf@kB3a>?k%{AD)DfVQDQj zfbE?!>D=DBx;i71o>Sh=ojYAW9X(M4&^5GmQ1!9G1hx-WnGlm`QZm_8LO$Ye@ovd9 z=FY?<9#VCUNWR3;tO+e%!0n^Y1h@%jn+JV9-#|9qV)2uS`yTe_FYTf#{>LdbRUrv@ z+d>k=N?r|E=2xc)+d5eRW3PUUj4mZvGKAtY5yhv|ru&~ZlL`L@L44D(F4AS1+P3u7 zC)t9_xFlFO*cI6U(D!p6Grb9*AIr8sSgu@@mUJ)_MOD|!*eABvTY(nuJnj#tBNn4M zGpiQ>(-G7kwF<`C;Z^S?`!S>AVHSs>p12G)k5G z;L$(dKukl+R?1fcP4$ZrQ?krvw~`ZJQilT>To>7bYVBonaPanov3foVNQ`-fM~X|jx#HaY=I zq2&yige}l5R+a9ezIACm_u$d~p+57AOf4Vq#byyp7pk%2J4Vh+njJ9>t_yc)Q?zkk zNmc7_uK$$##iXdlH=5e)VVhEk&OnpT&wVG<(G1%SlS8VE%)?S!!R=yGyKVBBFIb8z zZ@{E1<(O5eVj=ZSR8)vx%*(0|uuV3}7x^s2enEi!%?l5seE&cJ;Gv|df6!g@1Gq!K z{dEP|wm`y{<+A|91p&7FN^yeyDmk5W<_wr_ zUyg2JHrr`F^8lRn#3M&rDZtV`3mKF%%$QB}%rI60=Th4*4I)BHf*dY!}rMcR{DvgqvscTM0?Z*Q8tQ203H#}Uu%R+C$o z*Z*oC&$XTR9yV?J7g@*G{KQ2+O~P5&RzNxcpmhuuUgTr-hj~Ma3Jy%2iFi7wxjS zw)5Y^T3o_TPM(hv*K8JZk5$s1U?FzIuFWzV84{s+EarfG;m#;OX5k%6gmjy~3 z+RZ}8>hbXL17_K}@N1Y$6D{Ze^9QW--+WMypk}4JmV1?yey8uptu0n!Zf@@m*9t=h zvB(o&50OX|x*;FrJI^a`z*Qd4=D{;DA7;TSB;+6aW4};xHI7~Qi+)~Vt!ni3KyePv z(C)x8eik!}AI2muEc1Ty&3&gd?>$V8=vOy7CLTgSKqUi}3{*1k-)7(+tNc7EYtMK600000NkvXX Hu0mjfC>0cC literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@3x.png b/Images.xcassets/Chat/Input/Media/SavedStickersTabIcon.imageset/ic_favestickerstab@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..64706d76810bbecd16d0bfdd0bb694c3e4c98408 GIT binary patch literal 3482 zcmV;L4Q29)P)Px?R7pfZRCodHTz_y}MIGOLcbBGVp+7Ppv~>n(6BJRafDFp0{DXip>VT0cOqtT; zPDn|U6lijhpPEY=NG?sA!X+t@jxZYF9~A}}XFx_}5Y#e?NREO?OA(~B(uO8?@2#Kj z>s|V8-`?+gxmV}j%+1^V?svc6&wk$b?e5#R?-;8b6$UB{R2Zl*P+_3LK!t${0~H1; z4CKoI&$mK3^(Q7Ks@~Xt@O;8>*6E91YG`PfDJPzMe4j8xhlWPJCgS2@G;TRHkaND5 zdDwNq7XNSaX>hr8PY9wz+ehlzjMyz0n{Oow;6G+ne06Yxf6^*y33u~jCS zbW9n_Z2B$~xIUt{42^v$9@k&U#GTjcxo~q!;~z8Wl)KFJ5v|9KJF}4#*Y!KH>6E+N z)e+sZb@V(#H(ttxB+a9p-?_2rPr2xpL&wz-Eev6cRLB>6g%)2(C`@$ou zV?z8zCWmWUBguqejLBs_&cAS5n}4?~cSGK_5gilE7R{JFzg0Wm*hN1|%LJQG%-?uNW8i8kWwb|Hk^t-ZZ+`J$gBNlT>P zPUh)Mb*QdY=9aYUhz<=scn-RSt7U>TjrUsDHYiOpS&!--vtchOr^Izcvv^z;&HEN; zD|X66C1m@oa`R4r4XexzX;%>)+7|hgV4}fB4Ch-~TCSLu<)#c}vYc#?Em>a9ysL<2 zasES)k&214{mT~nM$?gGmO)1<*AliQarv^TjVd1w*0*)M4(_j!A#kSkty{P5s4P$B zsf=`zdCU2lgiVX6d`Z_39XDpW(7$@DRA9~d-_^?(MXWMansi9lT1K|Wrd3wXv@3}2 z8;YzHf?ca1;=cBVhL08U6j4Iwz7!!cWRq;m@^a)|L3B*#K{VcLB>)lnzOT0Weyc1~ znslj-q~lH4mc(injGy7ms)si!sn;Q=2188`7V&S2g5C4fSqgJXHM?-gL zn&)wyiPySXn^O97N!O9AV?+;(jV+B$iF#cZb(q)Ialz{Z7xiexD!hX_Ia6!!#Mj)} z>fez`CtX?3;K)seA%@eHn{v+ni}UNiGzm;m&Un=0;Za`=pKNbzJdm+&frUj8Jv8*w zlaGjx>zKjn0l1DEAeFN^EIDARCYBb03Ag{#Ao()%w+Qz94|*~L*-s&=FU`L;)!+x< zItuSmp>dpf6f^tD1$=(ArR6%hKm{E+6Wtq$ED}@vDm3I`yz8&Wkd8=YrxxTjO(jIu zleG)P_pmxQl}05eaU>F{`QQkD6nD}UIaSC}31q#8wnx$SNd&_0c#U|xBM>;0OZA+H z?j0Qasv(TWK=LWM7|mA)AP%7c_gu8PZs?LrE{W$`U#0)dGtYRR-yLlMDR&?s7O8ZK zmC-CmK;CdPrX@aSD*yI*%aNHD0@ZIv|m>(iK`$XKX8(_=FDu}O3G-j@&l4DD}-e9wbN+O9>&=u`pYO!^mp^B ztD@~UUZ1k!rt(`VS>gijf<5{nzy%LKuxr`$(HR+r6zj=DmQt`J!1!D(K8zQxib z#u2)Rk7lZum1qz)56+bb%3@6LH;;|nAk*x>*rd;=MK?9Ce;av2S+R^lV4~L0g{I%L z_{|=;2wMN}&QHc-29aKmJdH!XG-Vy) zm^I!JZ1)eSEZ?%BQ6+Y@ED;!Q0VMr?V+Gqi7}Tchdj>~u#T3>?a-zLq;wW3&vl2}PR<2mG8CPyXWejBB+Y^qs zW|d5Zu^1V(4FhSP$_8<=YWd<0RhcZyiDz@3#$`dKJfc_Y36G3nfLN#0!)v9kHh)f6SXtji!tU@$FQzD)Rggg1 zcxRix5oG5)*yj|{NoQmj4%TBt9!bhl&H#qTU*+JKqUZ9A*@iSEQ`?Dyd`PC|<`vON zeh}HbCp%($p&|aBX@z>JCk|q84eyes%41CHvHs3as?6x)mX-xT)NDcYV6n~VWCtrY0 z=@S;hl#XO9_TN$}lBo?PBbtnLwFdeTzUom~h;K*t9qcAOHykDwV3M(zgE!h>wnZ*1 zB44tu!-^p^7ztoi?|wu}ihXJXrhXS<>i42mf3DLQ_%6T{?gbS-tkT^WYz<`Hw39F9 zl16mWxd~L`>yb}+a-xReSMU@mX}{PrVUS5Su>!LbBokgoh$h3qjZNcRV{MeS9^m@B z^i8)_HkWDgO&DaKgq1MM@>1m;BborNTDD{u6UhCkU`5rQcMg6&RW^6IW4=Y|g$70w ztAj8cC7R~WctU(t#*aDc>9TCD@+#ki(dl`cN+lF>} zkyvU!vOVXC#+zr^sqp&t%Cb4j8%*WfhOyLm93?um{h?Zf%4t^6^g_3GUW~RHl;v2m zfHuG2rLyH9jD*!9DtXdTqM0#$t^)CE<$M%R_ld!myvIdf(lOfYLJbs#(Hdc@d zpQr7;t$E$M8PwAjVcM*P21h=^%ZilEj$WU4`R1F~zmv9N5e42N>SnBJ#NxL885RDX zFUk0>`xJb(yaCHm*T$x2R?a%bV+Y>s?7@2A!_4d59&EYc{n@fC3BrK)ov-3`+j&;i z1m+WqXsm~xizhB?JLtXHD=C5u8QLyCB^zXmY)TMjvq~%q%(E4WOgKU`8p{zZ=g8PJ#T=RjrtYngHGkMW`43^R2asPT z*E^o)Gw5Gmq#uUEnd9=*6xkqKWRq-@k5mnM@v4~512c3TnIJe~v?u)FS%!%1u{I8l z4h7o-i>gv#~H?Dn;Y}|YQu2bVjj|S0Y`~Z`T4NT56ywut_ISpNK zBOdc`@9mpg*V`9>e2hkIE&-$xYfNqjVu3@mx-?1+5&&Jm!_8_E=WUPIPyh&KS$jWK5|kYbfJOeQglbQ zIWvg24dQAm`AHoi8W27$!$1Qv407gfrr+b6yVRrT^*emb2m z){1_GfeHf^1}Y3x7^pB%VW7f5g@FnK6$UB{R2Zl*kP`#{1E10aqo+ik$N&HU07*qo IM6N<$g6K=P_5c6? literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/NavigateToMentions.imageset/Contents.json b/Images.xcassets/Chat/NavigateToMentions.imageset/Contents.json new file mode 100644 index 0000000000..84caad5eb9 --- /dev/null +++ b/Images.xcassets/Chat/NavigateToMentions.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_mention@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_mention@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/NavigateToMentions.imageset/ic_mention@2x.png b/Images.xcassets/Chat/NavigateToMentions.imageset/ic_mention@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f49e918fc7f282b61d78f339db5dbb7be6f46162 GIT binary patch literal 2215 zcmV;Y2w3-tP)Px-VM#e5S$}X;#U0;$@8120p$1WkCasP@9A~UJRSL9P&4|jV6)Mv(Whw}S z+(00ZNFpTdVB6smQYLp?p~Kw;LY+ZdTBVMoOgpU^t1?rWPIa`^GGzvu=0__?;+S7| z_jdc)OZMGf?!DY4p#7tHGdJJ;e)qfI{r=kB@9q&Y{Y`sd+5^)b_+NT}Ug^pAgkoRR z3B84JKATcLn-g*k=k$`M@otUM9;1MssjaB^_)5)t*#>ZE7YIl1ql_*ioIgY;y}mg^(dmX=r|N(KF@c8n^b?J?7HS2d>gw-(qV`SHZVJZk z!$NO@A&bZHb!sks`L61Uzl@{XqInw-2u6;=>uWQlP|{XF+;ufI>-sY2lR%n6(Upwx zLl|;xdKmrKaPhCzSNqSVY4)wW41l+GGtQooB9PM~glbg{+czGTw9A1_!S-G@4su%T?u(sd1vyhpv?JV%l9b>&_XoykCdxQ-}-5C z@R)@=G$u5ly}jMbC=1E4VnGs}uWP8Td|lG66s%jbX29+9ejCyKrAW^_OQh5 zpv#UZX}x`k8sz;=5=V&lHE#1iCviI{w#%0NM@kX3uQI~Ff&=TK&h#_7&;6%dyNl>= z2}YME2tN(kqq>{PR}dFU{_8^dY+XGpt8_Ipc;w$_%H4@q3WKrng;O8X^EquT0SH5;FoxC$L3FvR1_ z!I)BhY`{(@<@ZG**Tx4)j4_7&-Ra|AM04wCFH9W!XxG3FG&2m)UHXrb$>bAoUV&ia zggpshYk=b#z{$8u3?BO~EV*O8tpJfSQ2rQ{vs1ncUQb9Nc5v9^&dP$Pw{GYL@mC7} zy*P(b#wfH@)7AiyK9umK7PCfwT~=0xvLO2lgxWSk`3GeWChkYb{k|{gvl?stC58Hz zX6TxJFLLl5$)iG#dnn5Q(GSC4k$jNvxip~PFL4#xrqGa{SiqC%QbFV71$B^YkFApy zi3476yY;(vY^gXeY1P#akD&d>j*gCnr`|d9BJ$x|vQNO_VZ+`4ANd^_Ai`-W9kCnU zjrPl8jQZI|00r;d3?`TUHx-xdL7H4B^Hk6@jcitxmwW<>2vXhH0N>_y7uf0d_k-6e zwGjn%JzAn|4Pb=cEIHtwq53WBdnB$x%E&^A3%51K4mMP;{|HCPAJjg#F>D)w z6c6Q>EM7{yR$FP<8bCR_LGo}yd!6=R?D4lrKEMy1)``M{qLsX-;_(st6cRj?&_2ma zIiG|9T6!Qhu^CcG7UbltLc3Y)ND6M-#0Hv?o`&Ia9s_}v5Sm_-77Y#C2q3RNl1%Wv z^JPRM;>1jSnCa@h;pjIcZU;qa0(zO+7atk1H6R>*)dP1dk-Uh8ZFy~L00v%=1K|X{ z<-n2aC9Xo3VZ05TNg6Zh;h#MF>{SvU4^f&l!>3@M66N)H|UNR_o1nr-z17&+&anTR{(bcyMks7V# zQ3jhPd23HF(u1(SkJKvR$;2(-w{_M?++2yikd!B2AQXL+GrlHGqpvt^B(_4q%<%iI zC4kdqO+Py})54X8xPbsaAwbz4j z$m{dY&-H$@_os(%V9Z>^7`+oxZo{}npTF!P|s)K_Xu}*wS7n<}u z)#>JjZ5vmjL5iJ~qB%x{gsYMIs zp33J`7c=HGVBdkZ8{-Lf8b(|vRYaa2TyjV0_IxLi<30n$c^e=! zf)PvcuK7#k>ou7gV$tECLs@pBzNXUl<|X(=eKWwj%Q!P1gW-31+^&_M`}2BBI5IbB z(iahl(UUjR5`lFhy*@;=I`N9sO*L{AQg1f4%sHsB7U6&^ltaMxS06F%zMQAYNku?T zj4e<<+ts%gO8yXrlt}uNASEw1*7y;TseMy2Ku#sf=zSPzR}OkVQfd9aex zh)&zpM$z+CRpp;bnUjTiO_-0+)Cp{wE3igz0+V70Mr8OLwEJOKFJO;G_&L0*;y(cX p#ty$<6pPc}vPx>c}YY;RCodHTMclO#TnlH?s7mt0r9W=ShS@x^`}KfMi`2MR;+YDEfq({>cn0W zF(Kw+3CX3l(<-?bC6|N(UXl{UI(AZ-Do%k~+gNEur7aYwQb3&!0n7kR5SqWcZ@14T zIr(mP@4oMnON5!*nau6J`@ZkK`|P{#{(T`Noze)T5lADDMj(wq8i6zdX#~y*1VW)u z#yMd~zY+9{Fy`sn`pL}X3pn97p#94Tp_d_lDdpr+PRLl4yFfoiDLE>B3Fk+NLHBV& zH z>Zar%b(6*Z<+=N9c^xpM1R$|0MI!t+=)kN*ZDN$)qLgoP8Dwj5LGDM1c;&ihHf|gp zKG}T-;p}d#a*Hq!7sx!Hmnr#-8peJ8$FjG4P8(om&;V6bH{8THdj+F>Nn9H!dD}%@ zPm~mT-;T?ZM8&hA(8WC+q>OX=C zlePSm)J>Z-e)-a+OMA5SDI^AJ>h5NYZ^S^2k=0^=I6Uc|(!!iga-Ea%fB;&%wrNaz zPdhw^PgfhET^O}Ic+Cd2Zs;f#YeLtVVfq@T+?7fzlyqbeI;Yr|vsdPdAd0Dng{nq-DK-;9uu^ky|_fCAO^4*;AyWFz3l zVeWD*fh+Hn>xQfB&wuD7Kw1o2bVraWgz@#s+LF{1>sxBO4#0~h$_*KmX2Elwm+4_N<=KZf_jf&-1{Oi?o61gh(1a>k!FnK@r)kf8NX;fd(nyiU^d_^M#2 z7DtVJsPe_gP|{lF%egCtW+{vqk@3^6?#O-67;{8nlmyVmAH){G!miCa@*r``cd~=D zVx-4gwLo~AzbID;*j65EzC2i0gmNW}b6M$w$R92%%Dp0ye4r}yEFu`-T`LM{)6)&ZE+DFjD&uk8SH&&U7{#-|m7Omm?^ z(-0g1#0bAvmW6G5aQ(SQrt8Ys8ms`o%XD>7TCclPn$$G~z_tZQEd7LX<f@)sv&xA)ByX&0Xt+qG%VpajeehV@9PB5?DS~e52q1XQYRD>{ioR{99^>KC zJntc^?v(}EEhvk>%@tJ*FEh%XgQqB43n7C=!x=y>_cjs`!F6Toy|?44Es_0nX0Z)2H1cgCp~m6nS4K8Du^h?2zk{ zDPv!B93G?W@B#2_J3Y1lLCIGXC6xYKp=k(=nM)M1=xhdgVE+915v5K;@aN|oz`z%4 zst{VlSUF2L(rZ>MKFLnn9w1IM0CC+}N7I%X2yr9{^>tdq9*5*F^sa*ksBJoqK~T&I zu5x1&V!U?hTDfci!WgLl5vm$%)rvf(mI3dZDv?3goVH~qsSF&Tio7e4MI(+q2m_-4 z1Q(2d3)FEPMZhrF`$|V05HOomxuiUZDFC4#-F8je0t5xB z62nkBE;mFC7n5nC%vpx5tWJ&?T#k2@i{ZHGLg=h~DrN=G&eObmPC^U&s zx;*Oy*1H~w43arhp*bL6v=uqTCFcMUJO-&r-C;{S_5g8$j>aZVxh+5|X z0*)#;=%&bh*x2#`VY#@KW)X0M$y5=CQIlnquLeo=2b~_QNbnJ`xxGQC;zV zb%85}*YFrySvM{(EeVZH2vl#l5u_a!vNns0{1ud$x5@?rr0en0u(Q0X?m6_c8Q=T- zO8tMO7h12N-X41zN1F^zCOCKzlSm*~cERZYXSXdhR_u?eI5LM%mJvh{-A z^AfIA6>;wDKBeq2?>-Fkql$bV!HpeYKE4Lo#CXpqCp&1j8Hw!XCfk5bU}A9k5QRVBJgO54E&y0no}cMyG3jl<)nIETbzsn!~uZT!3BQ?^Ol#-m;>cCY7$I zh?s)WtV56=spm*QHRyAc@w+fpE?2lH!7GSSf48^T-V^|rUYHn_}jbl+czc1%S>LK5Rt|t7+VIj&E^9#Nf{5@D! zn6nH)`LUSFXhFQmb5<51k)1pe{uc(IS`QBC#@_NFr{DPAPT;WV7 zdMc{xA2c~z|2e2OjlssFm@3{9U(Qw=R;Ai8Kti%FhTt<3CDC^;a8I6BP_R%v*eblK zAyn2hPK&Vc&k)$fgF?JeAwHs7KgKoMUr_MCX9H*h%pZi z#E8Mj1cDo`A~fH7p97K!#z8~5iY|lmD1_hAlQ1AZeNZVR~bVg+?M0CLE2PRWTzx$Y~R!JVR%Lx&UX`VO6&i}y2M)Gm3#2f!`y*x0uyWRt4%3D zLZ$fUxnu2N-0_f797`^8V#Hv00MeTiGWo)x+ExsB6^qxYtgNgq?5Ll0jpkR0k2`T3 zPCDMi(=F3+jQAK{*Ng|RzJspJkb-i`JxI3cne`hl?dj^ihvL6`{TH_c zyD??ob`$sZD<@yD3)dg9m*i&!Ql1-DfOOXP?xoJ~$u7<(nTGg}XWr^&#-|?QK9=Y* xUZ*QfOCyj*AdNs8fiwbX1kwnk5l9^Z{|BVRCZNCQw1CR_KMq47CG^DBn7PYezJ85-V$RX&9(Zg>IG4%7uy-~0l^ zXaFLhGNAcjLm(D40~J8b1e1*r?LhTlOK1LPs0T5emwfmHbfIZUkY6wZf8?Z{ci;S! z$=dbn|GdAy_mq083bP4xd|MUm_x9gX#Y!WNlA|BJ?rZK|{QkOset7l~*2#BnUfasX zz`!`y)5S5Qf^qJ&(5B4^0xZ^DrthStm+t+4weI>{mKkfedmL0xp5U`=^UtJAiPJ}G z%;qKtT;3%6uQ@q7ZU2L+CGstSbCzTZxhFoDJg++T)Tbu5$khL};ZxbVZ4SG>YRlc; zlyG%xPF769U85Thlx8=)jjGxG_Ql(RsdJC#{XTf(bAb#0apv#;t!4ewzZBVR{GBgv z!{Kbf{%`aAU0?eop1jymW5aiWCKkG zxf$$okW-&BG=Uuk(N*`H0f>MiK-adu1{%BJ@4Ej$Uk8>1`2{n`2URXOcJtY%KRgk0 z?*3s ziT7GoiutPqzw|Miem>RsWr&gB^O?qCSF5;=3NbwO`dTftHtZC`lrGk_3&YqmOrn3q zYTsm7aea%I^-D&pyK~MI-Q|1Wxh-|&b#}JH6W8>%F)z43x%DH?+qw`wgcuRP;pzL5WVvkK3&TgQqRJZpnKA8@)I*|2ZJ z-nOpT5VMk1aJ`EBl+@h!cKX?=A3Q%fyY61q5PwNHWb0}zmL;|w?ps%9$b8$v&pGpN z?ol2S>5e81t6iRxVv97k>3eQou;QPa=o8hO|NdVOsSvq%C$QY^LBjW$JHLlmFbBvV zj(ebKrSb85OxE7C2-aN@2lV9p&Tc5%(4qQ$Zp3eF-LB*P z=IXZhrrz(W3)?TPDQkaHJ7rCo{L#DTHgVZ>+e}Q|YO}DoFKz3b#h<6AZT+*fIPq!u zsuQhR>+ijL{L}1ul)lpXE}6Bg*qWS3CQ>MyjUorg7?@>$^Y1tLZPVeQ4uzSf^&;#pJC0RlAtyw!~k(SN`UB&-cHK X3==Ba|7%1X0u>XUu6{1-oD!M3_#+qP}nw(T2SThqC=ZQHhO+vl9ldUNb^dh#TbWY)XgzfIdGnWUNdzHi=l zJKIj@SMHo2??O|hxh9!Tk^7g%wJM8s7stip!g3QWVFCm#sfsJwXhyhFp-+?~lc^_&A(k9+OM$IlQIQ)bhIwS% zV2bPf`kfSzyHP*``Dj90&4}ECe!0A;qJS1Op{*uZx3M=lN*C5IrlNorG@%WR{lX?w zDsn$kOs4KU6%90@4UK5+N1;>v-TYHGB-5y9pb2egL~AN+)BG}Ss9ze}Dj>UuQ3}vT zEwrLJ#SZ8(zbu{>6x1l7k$PzE>*IS1Z*qjJTe$E>(TG-RqPcWx!AnZ!b&& X=s9+M@@wPd00000NkvXXu0mjf6lR7# literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Title Panels/ReportIcon.imageset/PanelReportIcon@3x.png b/Images.xcassets/Chat/Title Panels/ReportIcon.imageset/PanelReportIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..081f61a1d1b0006cec85842c06ec01ab8ec50b26 GIT binary patch literal 1242 zcmV<01SR{4P)&Nj%eSC>Yr3Rfa9FEZLtgd2c4f!Cb9X>5qBT?4>)hzVuyX; zU&Xq{5$n}PZ*|Vo&e~va38cSBrM&nBRBc3w^jprW_F_o^?X4|-@E5SJzC-IC47z$J zk~GlXec-@fKoEAy`bS#5yEFlI-lOuALEPBfC{`5xVzZqw*kNyd;kVK#vvOTGVtel* zG8nLTyC?BqNlvMaB(Z+i5#l__0Q^{=_$N-~h?3a3!hb2sj4v60AN*P0#Hkb=J<~|6 ze{wz&c9IOipY=@~;#G!N-|Ad=p3Qo=9Dn$=F^E?Q+VonEuzyH=r?)b&O zZBN{m=7icP5}OYlanBUUL0rJUZBJa{w=BwZI>q*j`exib#l=5ytPSy7l8(L^>PBx> z&$a@2iVMVXM~}&)<%rB(&IRU4snz~BEW{-a@vI$rv=k~?-zn~QY44jyEcuEH#3Qb? zC7+hy*q+fO(w{hmogy#{#RcM8Tk;{V(i0~~JFnUvIqJ)?hC*E865raBSE*5EW{J$N zj<{P041u^nd}~i$qa>zNJ9*%wGSt>)A-4Na6x{xgT5P zU3k)8F!B;N{ROwssAS8!Tb&LwOQFRjKlfIV{0oVHldh7Z1d1#!kf;BUQ#j(}RR8g^ z6yo}i=hRUsRNvh79V2F4fij2- zasAGG>dKY$!zk?nWfT|4-yen=GGcSKKLA*2as2^Q>dT9CL)SfP2$Wb{ppH~S|4^7o z7h%#$1C-ghrKbz|)2*mC6Vgv~J3?9!>hdQF=8n=ARAP#zKgE^0bH$%r+?K2&tqk?0 zpPZe^wD{|#3Q(uNtRQog*|YJ2j`H7%HCzyb4yj1L6zO1rDu@fz>o4WXjM!Z2FQRL8 zaT_c`yfmM^4^;2+0u>S$s5{#|9{h#NN?#>VMR9@p{e|CyC~QA}$$h28^|vfQpY-^f zMOa4xbnrIYX!

wmE6AO$<5>YVU*gWGM+)&?EgG56;$74Q@LE=;Ck9mFk8$Hs6Fz z(f$Q=L7xFXSap6eg+%82#*cadze@n!(64bPMc9b4xs5#0ukqdoVGGr!x1dGG^c};2 z^aBX)kVg2*<_kUTU|ghs%+PuGLQiy+NZ*o6fbez8Uw&q%;R=svoB#j-07*qoM6N<$ Ef@u6}-2eap literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Title Panels/SearchIcon.imageset/Contents.json b/Images.xcassets/Chat/Title Panels/SearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..5e19672ebc --- /dev/null +++ b/Images.xcassets/Chat/Title Panels/SearchIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PanelSearchIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PanelSearchIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Title Panels/SearchIcon.imageset/PanelSearchIcon@2x.png b/Images.xcassets/Chat/Title Panels/SearchIcon.imageset/PanelSearchIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0560d61dbccd7c1932057bbd83484c1ce08d264 GIT binary patch literal 654 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|^t)d4;su0Wch^)*A?Glqs|4D~?t z6huz?58?s^LFg$%!+#hXtOg|VoT1@0L&GbmE}%#w(1iaCO|EeNbysZ!UUjeElYy@f*B;!m+Zg! znLY2szyD0J55BBTc2MJyJAE#Y!}?`3W5myG{5>ZFmVWwr|J1THl`H2{UuPcje3Hc} zzpFY=cLoCkW2C2xV@L(#+o{ag-WrIs1}9&CSD!ok^ZXl|Pug7*t7V&tL_Gm5h z^4r>dUw83Y<@>tSH#}TnWh0ezA>SgoVPoj;1BPr{pC6I!iEKXh;>w=mUItrc31oLH zvC2FwxzVjx=7uP@)r(DHr4Og%u2O!xRdKIKRc1z$W_Bps;wPQUW^Y&CnJrcu(sJD5 zlY-bU;~zFhj<2~eUGT#MhN6Y*6`m+6PH~ENJ>smiqV?Y&(G^TwXXuqHd!_{5saQ z=t+IsewwxS=e8v$K75`3)AUU98Jl9al#C

uZLFXABL`8S0-g z)FGi}px`sG;8TYG|3Pe^Y{LtXNc~d~Y5<}a3{9^XI$txa`VSTX8uApXY!Xn*ONKh2 zO0YStU^dW%=2r|e!KMMln_hy{z_ox3gh_#Py#g5pvhF$9d0?YJZg|bm_yTMT*kquB zwog!%-C%K$8jv}9mmIr+9`q^+@(X5=jOti_<=Lk{nr+wrGX|aTyZU`^LxdqObECrM zRX*%4?|x4c&t0R+I_sgO)$Su=^Vn)me&%_#ztih@>pwN;OCF0}3dbJgK6HR7E!DiD znt_2y#M8wwq=IqInb1W_fdb7BAAEakwRVT~oBG_(7DcOP7ypo3J?TV<#KsCK~-Sm0`H>{-nDA6VD=x zi}xp4NHMfD&RJTVurcQ4@~cIRXM!!Gt~Z_y+xC`i#rmg7+u8ZtQ>|qi{+>&G#S^pc zRPkPh=jW5FAC!gf`^|gfb+hS*3+em`YhyBH^NI?at5(Szl+i2Sxg$P7P9vJ5ef6Cf zegUmuev4CAIbUcx@8*}@dD~&XZZx}C^v>IQ3zTmQPy4j7Wt|VN#F{6`rVVX6z8z20 zo0jS5c05&Yo5sT`q4H@m&w_+I-3C+ED~j|MbGm&pZJJc5cQ|6-u>iAkqBBlhXVE&g zlZ*AImcvzx{SS2G^D88uTb+55&DMSY?F?q2!vPX4_ZhRK!#i^C{dd^9bM8Bf|2us5 zReX$iZo1}b$fLhm!t)oq)$C7O^Sv%+^~)pgFFtsAr0T?F{mCm#t3kGIg=Pf!jX&gB*LATNGcFinb1v80&%0S#>lC*-H+Pxz z;otU`SsE5L&yQqXo$w=M!=4|~W&bX`4dP&X(~@++PlNAG%c29Hf-XG!!m{1WI98(Z SrX>d`UwOLvxvXSa%U9KUQwqA?zV_K`(`g!8N6*Gi(WUbBLRq_q&UoDfVKDY6E(j!PC{xWt~$(697Ug Ba6|wA literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/Contents.json b/Images.xcassets/Instant View/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Instant View/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/Instant View/SettingsIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json new file mode 100644 index 0000000000..a738861b67 --- /dev/null +++ b/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewSettingsIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InstantViewSettingsIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png b/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..48c1617c4bc13b9be1021cb898240f4f375694d9 GIT binary patch literal 829 zcmV-D1H$}?P)5ta9+8qMS@r)R>qkV7!=!kHo~NAYM#Vd>l+bd-NGK z@i6~xyLf0sz^FX{TH^u5S0eaGAc%@7th4`2+uiNVQqpPX(4Co`|C?|AZ~kw6zN~&Q z6aEm>YFdpiS3j+eK@bx-!|5SO#X(8 z)$k}KaZLId2k7FsB zkzt&Ru31R07J%9kIa>hE`^Q|5(mSy%_w@;Xp0572|lzu1K-*a(IR zuaGG3HO6R{o%Jd@`4jr+J~mD0h0mn(N7yDpOT{JcDxJJW8Y$E*X<1y&limtf#7%2v zcL**DtC3X}LIGF=<7A|Ly5umL?S|{wL_Vo+q3Cara2tK9m3ZM`#vmqaPv7H1nu`+% zvY)>2M95<=UmhbbJlhDqrMZ@ei^(Xpol@j9gZzeZW1%3aZ7DDGlz9VgChqb{IA|r) ze%+u!>V?N90(m^2`J{H*3G$o{?n(A4DM)V`$a&_4`m7A9LMY8AjWQbamtL-|WR2dK z0jjihF7M_-&)W=h~6iHB!%FYLty#-x0wmM1^p)2d~KnLMIokK^m> zIrUwW`Zg=#KH1`f2z@vpJXNYEc!hAc5#)!+3q21k$b9lYEYSJXbNp!700000NkvXX Hu0mjfP&<#C literal 0 HcmV?d00001 diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png b/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..736367a1086b6965caaa359167b8fa808602db2d GIT binary patch literal 1133 zcmV-z1d{uSP)001@!1ONa4CHt&?000CuNkl|qn@fEyAnf&-g4SP2di91s^IT*NxqO&rwg zG2#sBRHQ1_7EQ$)NdkwPIWwO>y7ez7yMbSIcWLew* z+UOVA!`(PwswhVk9HXpOXvP1>jwlmlSu1ea?i}wO@5!CNY^^ZU-2v@0OUtuZ2Gc|> zrjQ5prc&x@rD(l}#T2t_)rlh9o=D#lPb6`py*f=1<%xhpl+cAzyYSheC&CPOFF{*9 zE~WaPE~S!3bo&xB4$>L4%YtNS7f|ku@h4Us+~Dw)61kNq_r?)tw5?iD)Tt{*T;qx% zr&!lw^e)`Np$BD>2hm_vdxeqqKOYiF5wss>{P6GV*QvYJPP%S)~q*y zRY@({;?nU>ai6{;m)NG(D9>8MAtzL&PoTZl4Chg6zUnt1-(XgSezb#2vz-gu^&85g zT$?6Nw;9N)+!ryAYED4Js@xY=56Q*ubA$3Mf;RY}yWBr;48rOmL)iTgyC_?-z@yeO zUGymvSk;!y9H!=pi-Kt#Af=^2E&3hha*ZDQLXzd{xDol@RgPm_(PA&(mk=Z z!<9w0wLGMknsg4!bF|#6b7JL%Ey{b_^3*rETlzV8{+zhAH!zH1=NrL(uz07TlONR% zI#0Kv{ZF={`A9`8*mTB{KiG9c(9F?*Q~IOAs@xQ<9+AvugxYUP!yjw(cvN!_i&&Mb zQD|U)3s>?r=KTh)>~B?zT0`1ln^`5izkqBNPB zFojeZlnXp+O$<5hlqj$&luHNL*Oj%eu!Qqi`!lT2jTA(kx} z?ormKuw;G0e(puA`kXh=3pfLE2Baex9%W@!(&4w&8#t#Myij!$7t)!WTi+=2DCr41 z@a>VYDpc`lR!uY06fIPVIw}g5M_E~w6!AlXG$X7it1B?k57XF<{Y4T5cJ!}lA5|s! z92cJK=~^$@(|zX}+%G5jd6Zn^iBTelegramUI.xcscheme orderHint - 13 + 12 SuppressBuildableAutocreation diff --git a/TelegramUI/ActivityIndicator.swift b/TelegramUI/ActivityIndicator.swift index 8a5bfa2b69..eab0667b27 100644 --- a/TelegramUI/ActivityIndicator.swift +++ b/TelegramUI/ActivityIndicator.swift @@ -1,16 +1,64 @@ import Foundation import AsyncDisplayKit +enum ActivityIndicatorType: Equatable { + case navigationAccent(PresentationTheme) + case custom(UIColor) + + static func ==(lhs: ActivityIndicatorType, rhs: ActivityIndicatorType) -> Bool { + switch lhs { + case let .navigationAccent(lhsTheme): + if case let .navigationAccent(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .custom(lhsColor): + if case let .custom(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + return true + } else { + return false + } + } + } +} + +enum ActivityIndicatorSpeed { + case regular + case slow +} + final class ActivityIndicator: ASDisplayNode { + var type: ActivityIndicatorType { + didSet { + switch type { + case let .navigationAccent(theme): + self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) + case let .custom(color): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) + } + } + } + + private let speed: ActivityIndicatorSpeed + private let indicatorNode: ASImageNode - init(theme: PresentationTheme) { + init(type: ActivityIndicatorType, speed: ActivityIndicatorSpeed = .regular) { + self.type = type + self.speed = speed + self.indicatorNode = ASImageNode() self.indicatorNode.isLayerBacked = true self.indicatorNode.displayWithoutProcessing = true self.indicatorNode.displaysAsynchronously = false - self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) + switch type { + case let .navigationAccent(theme): + self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) + case let .custom(color): + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) + } super.init() @@ -24,7 +72,12 @@ final class ActivityIndicator: ASDisplayNode { let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - basicAnimation.duration = 0.5 + switch self.speed { + case .regular: + basicAnimation.duration = 0.5 + case .slow: + basicAnimation.duration = 0.7 + } basicAnimation.fromValue = NSNumber(value: Float(0.0)) basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) basicAnimation.repeatCount = Float.infinity diff --git a/TelegramUI/AudioWaveformNode.swift b/TelegramUI/AudioWaveformNode.swift index 5fa905aa50..0530ade789 100644 --- a/TelegramUI/AudioWaveformNode.swift +++ b/TelegramUI/AudioWaveformNode.swift @@ -49,7 +49,7 @@ final class AudioWaveformNode: ASDisplayNode { return AudioWaveformNodeParameters(waveform: self.waveform, color: self.color) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! diff --git a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift index 5cffd5d1b3..a1eb44d1ea 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift @@ -113,9 +113,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeField.textField.keyboardType = .numberPad self.codeField.textField.returnKeyType = .done - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift index c548ccc36b..30eed41f06 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -7,9 +7,11 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode { var dismiss: (() -> Void)? override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white } diff --git a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift index b46e8d3464..f809898592 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift @@ -61,9 +61,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.codeField.textField.isSecureTextEntry = true self.codeField.textField.returnKeyType = .done - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index ca3edf8fc9..4386cf0cf2 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -353,9 +353,11 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.phoneInputNode = PhoneInputNode() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white diff --git a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift index 1faf50ddc9..74a92c4f2a 100644 --- a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift @@ -73,9 +73,11 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(rgb: 0xbcbcc3), paragraphAlignment: .center), for: .normal) self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(rgb: 0xbcbcc3)), for: .normal) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white diff --git a/TelegramUI/AuthorizationSequenceSplashControllerNode.swift b/TelegramUI/AuthorizationSequenceSplashControllerNode.swift index d6d8306297..c15c5ec1ff 100644 --- a/TelegramUI/AuthorizationSequenceSplashControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceSplashControllerNode.swift @@ -4,9 +4,11 @@ import Display final class AuthorizationSequenceSplashControllerNode: ASDisplayNode { override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white self.view.disablesInteractiveTransitionGestureRecognizer = true diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index d1bd443cd6..ea4bc63d8e 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import SwiftSignalKit +import TelegramCore public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable { public let privateChats: Bool @@ -46,6 +47,38 @@ public struct AutomaticMediaDownloadCategories: Coding, Equatable { public let instantVideo: AutomaticMediaDownloadCategoryPeers public let gif: AutomaticMediaDownloadCategoryPeers + public func getPhoto(_ peerId: PeerId) -> Bool { + if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser { + return self.photo.privateChats + } else { + return self.photo.groupsAndChannels + } + } + + public func getVoice(_ peerId: PeerId) -> Bool { + if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser { + return self.voice.privateChats + } else { + return self.voice.groupsAndChannels + } + } + + public func getInstantVideo(_ peerId: PeerId) -> Bool { + if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser { + return self.instantVideo.privateChats + } else { + return self.instantVideo.groupsAndChannels + } + } + + public func getGif(_ peerId: PeerId) -> Bool { + if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser { + return self.gif.privateChats + } else { + return self.gif.groupsAndChannels + } + } + public init(photo: AutomaticMediaDownloadCategoryPeers, voice: AutomaticMediaDownloadCategoryPeers, instantVideo: AutomaticMediaDownloadCategoryPeers, gif: AutomaticMediaDownloadCategoryPeers) { self.photo = photo self.voice = voice @@ -108,6 +141,10 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { return AutomaticMediaDownloadSettings(categories: AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), voice: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), instantVideo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), gif: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true)), saveIncomingPhotos: false) } + public static var none: AutomaticMediaDownloadSettings { + return AutomaticMediaDownloadSettings(categories: AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), voice: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), instantVideo: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), gif: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false)), saveIncomingPhotos: false) + } + init(categories: AutomaticMediaDownloadCategories, saveIncomingPhotos: Bool) { self.categories = categories self.saveIncomingPhotos = saveIncomingPhotos @@ -144,6 +181,19 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { } } +public func currentAutomaticMediaDownloadSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings]) + |> map { view -> AutomaticMediaDownloadSettings in + let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + if let value = view.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings + } + return automaticMediaDownloadSettings + } +} + func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping (AutomaticMediaDownloadSettings) -> AutomaticMediaDownloadSettings) -> Signal { return postbox.modify { modifier -> Void in modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { entry in diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 717fbefdcf..79ac0eab6d 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -142,7 +142,7 @@ public final class AvatarNode: ASDisplayNode { return parameters ?? NSObject() } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { assertNotOnMainThread() let context = UIGraphicsGetCurrentContext()! diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift index 3765bef34a..99cf284d76 100644 --- a/TelegramUI/BotCheckoutInfoControllerNode.swift +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -61,9 +61,11 @@ private final class BotCheckoutInfoControllerScrollerNode: ASDisplayNode { } override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return BotCheckoutInfoControllerScrollerNodeView() - }, didLoad: nil) + }) } } diff --git a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift index 8077614ead..a688bfa625 100644 --- a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift +++ b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift @@ -30,9 +30,11 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode { } override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return BotCheckoutNativeCardEntryScrollerNodeView() - }, didLoad: nil) + }) } } diff --git a/TelegramUI/BotCheckoutWebInteractionController.swift b/TelegramUI/BotCheckoutWebInteractionController.swift index 8ab030bc72..37b02c77a4 100644 --- a/TelegramUI/BotCheckoutWebInteractionController.swift +++ b/TelegramUI/BotCheckoutWebInteractionController.swift @@ -80,4 +80,12 @@ final class BotCheckoutWebInteractionController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } + + override var presentationController: UIPresentationController? { + get { + return nil + } set(value) { + + } + } } diff --git a/TelegramUI/BotCheckoutWebInteractionControllerNode.swift b/TelegramUI/BotCheckoutWebInteractionControllerNode.swift index b9f0487f41..832b829438 100644 --- a/TelegramUI/BotCheckoutWebInteractionControllerNode.swift +++ b/TelegramUI/BotCheckoutWebInteractionControllerNode.swift @@ -55,8 +55,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, configuration.userContentController = userController webView = WKWebView(frame: CGRect(), configuration: configuration) + if #available(iOSApplicationExtension 9.0, *) { + webView.allowsLinkPreview = false + } case .externalVerification: webView = WKWebView() + if #available(iOSApplicationExtension 9.0, *) { + webView.allowsLinkPreview = false + } webView.navigationDelegate = self } self.webView = webView diff --git a/TelegramUI/CallControllerKeyPreviewNode.swift b/TelegramUI/CallControllerKeyPreviewNode.swift index 60a5095581..25774fd058 100644 --- a/TelegramUI/CallControllerKeyPreviewNode.swift +++ b/TelegramUI/CallControllerKeyPreviewNode.swift @@ -2,7 +2,7 @@ import Foundation import Display import AsyncDisplayKit import SwiftSignalKit -import TelegramLegacyComponents +import LegacyComponents private let emojiFont = Font.regular(28.0) private let textFont = Font.regular(15.0) @@ -74,7 +74,7 @@ final class CallControllerKeyPreviewNode: ASDisplayNode { UIView.animate(withDuration: 0.3, animations: { if #available(iOS 9.0, *) { - self.effectView.effect = TGBlurEffect.call()! + self.effectView.effect = UIBlurEffect(style: .dark) } else { self.effectView.alpha = 1.0 } diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift index 7e34e0c02d..49a5bf403a 100644 --- a/TelegramUI/CallControllerNode.swift +++ b/TelegramUI/CallControllerNode.swift @@ -77,9 +77,11 @@ final class CallControllerNode: ASDisplayNode { self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings) self.keyButtonNode = HighlightableButtonNode() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.containerNode.backgroundColor = .black diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift index 9c74ab48d9..a6e3c6fdeb 100644 --- a/TelegramUI/CallListControllerNode.swift +++ b/TelegramUI/CallListControllerNode.swift @@ -179,9 +179,11 @@ final class CallListControllerNode: ASDisplayNode { self.listNode = ListView() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.addSubnode(self.listNode) diff --git a/TelegramUI/ChangePhoneNumberControllerNode.swift b/TelegramUI/ChangePhoneNumberControllerNode.swift index 68950b023c..d9644d98a2 100644 --- a/TelegramUI/ChangePhoneNumberControllerNode.swift +++ b/TelegramUI/ChangePhoneNumberControllerNode.swift @@ -116,9 +116,11 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { self.phoneInputNode = PhoneInputNode(fontSize: 17.0) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor diff --git a/TelegramUI/ChangePhoneNumberIntroController.swift b/TelegramUI/ChangePhoneNumberIntroController.swift index b36bac97fc..68019aa1ad 100644 --- a/TelegramUI/ChangePhoneNumberIntroController.swift +++ b/TelegramUI/ChangePhoneNumberIntroController.swift @@ -20,9 +20,11 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { self.labelNode = ASTextNode() self.buttonNode = HighlightableButtonNode() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeTextColor) let textColor = self.presentationData.theme.list.freeTextColor diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 74a1ceb4c8..383c6499b0 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private final class ChannelInfoControllerArguments { let account: Account @@ -404,11 +404,13 @@ private func channelInfoEntries(account: Account, presentationData: Presentation entries.append(.banned(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Banned, value: "\(kickedCount)")) } } else { - if let adminCount = cachedChannelData.participantsSummary.adminCount { - entries.append(.admins(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Management, value: "\(adminCount)")) - } - if let memberCount = cachedChannelData.participantsSummary.memberCount { - entries.append(.members(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Members, value: "\(memberCount)")) + if peer.adminRights != nil || peer.flags.contains(.isCreator) { + if let adminCount = cachedChannelData.participantsSummary.adminCount { + entries.append(.admins(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Management, value: "\(adminCount)")) + } + if let memberCount = cachedChannelData.participantsSummary.memberCount { + entries.append(.members(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Members, value: "\(memberCount)")) + } } } } @@ -520,7 +522,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr })) }) }, changeProfilePhoto: { - let emptyController = LegacyEmptyController() + /*let emptyController = LegacyEmptyController() let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) @@ -529,8 +531,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr presentControllerImpl?(legacyController, nil) - let mixin = TGMediaAvatarMenuMixin(parentController: emptyController, hasDeleteButton: false, personalPhoto: true)! - mixin.applicationInterface = legacyController.applicationInterface + let mixin = TGMediaAvatarMenuMixin(context: LegacyControllerContext(controller: nil), parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! let _ = currentAvatarMixin.swap(mixin) mixin.didDismiss = { [weak legacyController] in legacyController?.dismiss() @@ -561,7 +562,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } - mixin.present() + mixin.present()*/ }, updateEditingName: { editingName in updateState { state in if let editingState = state.editingState { @@ -746,7 +747,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(entries: channelInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .plain) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index e37c4c9394..0b5364328c 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -31,9 +31,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { self.themeAndStrings = (theme, strings) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = theme.chatList.backgroundColor diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 66ec3da1fc..80615b1ef0 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -25,7 +25,7 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat } } -private func actionForPeer(peer: Peer, muteState: PeerMuteState) -> SubscriberAction? { +private func actionForPeer(peer: Peer, isMuted: Bool) -> SubscriberAction? { if let channel = peer as? TelegramChannel { switch channel.participationStatus { case .kicked: @@ -33,10 +33,10 @@ private func actionForPeer(peer: Peer, muteState: PeerMuteState) -> SubscriberAc case .left: return .join case .member: - if case .unmuted = muteState { - return .muteNotifications - } else { + if isMuted { return .unmuteNotifications + } else { + return .muteNotifications } } } else { @@ -48,15 +48,12 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let button: UIButton private let activityIndicator: UIActivityIndicatorView - private var muteState: PeerMuteState = .unmuted private var action: SubscriberAction? private let actionDisposable = MetaDisposable() private var presentationInterfaceState: ChatPresentationInterfaceState? - private var notificationSettingsDisposable = MetaDisposable() - private var layoutData: CGFloat? override init() { @@ -74,7 +71,6 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { deinit { self.actionDisposable.dispose() - self.notificationSettingsDisposable.dispose() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -105,15 +101,9 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { }).start()) case .kicked: break - case .muteNotifications: + case .muteNotifications, .unmuteNotifications: if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer { - let muteState: PeerMuteState = .muted(until: Int32.max) - self.actionDisposable.set(changePeerNotificationSettings(account: account, peerId: peer.id, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) - } - case .unmuteNotifications: - if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer { - let muteState: PeerMuteState = .unmuted - self.actionDisposable.set(changePeerNotificationSettings(account: account, peerId: peer.id, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + self.actionDisposable.set(togglePeerMuted(account: account, peerId: peer.id).start()) } } } @@ -125,8 +115,8 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState - if let peer = interfaceState.peer, previousState?.peer == nil || !peer.isEqual(previousState!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings { - if let action = actionForPeer(peer: peer, muteState: self.muteState) { + if let peer = interfaceState.peer, previousState?.peer == nil || !peer.isEqual(previousState!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted { + if let action = actionForPeer(peer: peer, isMuted: interfaceState.peerIsMuted) { self.action = action let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) self.button.setTitle(title, for: []) @@ -136,34 +126,6 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } else { self.action = nil } - - if let account = self.account { - self.notificationSettingsDisposable.set((account.postbox.peerView(id: peer.id) |> map { view -> PeerMuteState in - if let notificationSettings = view.notificationSettings as? TelegramPeerNotificationSettings { - return notificationSettings.muteState - } else { - return .unmuted - } - } - |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] muteState in - if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.peer { - strongSelf.muteState = muteState - let action = actionForPeer(peer: peer, muteState: muteState) - if let layoutData = strongSelf.layoutData, action != strongSelf.action { - strongSelf.action = action - if let action = action { - let (title, color) = titleAndColorForAction(action, theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings) - strongSelf.button.setTitle(title, for: []) - strongSelf.button.setTitleColor(color, for: [.normal]) - strongSelf.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) - strongSelf.button.sizeToFit() - } - - let _ = strongSelf.updateLayout(width: layoutData, transition: .immediate, interfaceState: presentationInterfaceState) - } - } - })) - } } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index fcc35065a8..7f9fe92f15 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -69,6 +69,7 @@ public class ChatController: TelegramController { private var buttonKeyboardMessageDisposable: Disposable? private var cachedDataDisposable: Disposable? private var chatUnreadCountDisposable: Disposable? + private var chatUnreadMentionCountDisposable: Disposable? private var peerInputActivitiesDisposable: Disposable? private var recentlyUsedInlineBotsValue: [Peer] = [] @@ -88,6 +89,9 @@ public class ChatController: TelegramController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + private var automaticMediaDownloadSettingsDisposable: Disposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) { self.account = account self.peerId = peerId @@ -95,6 +99,7 @@ public class ChatController: TelegramController { self.botStart = botStart self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } + self.automaticMediaDownloadSettings = (account.applicationContext as! TelegramApplicationContext).currentAutomaticMediaDownloadSettings.with { $0 } self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings) @@ -141,7 +146,7 @@ public class ChatController: TelegramController { }), in: .window(.root)) } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { for attribute in file.attributes { - if case let .Sticker(_, reference) = attribute { + if case let .Sticker(_, reference, _) = attribute { if let reference = reference { let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: reference) controller.sendSticker = { file in @@ -235,29 +240,31 @@ public class ChatController: TelegramController { }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) { - if let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) - strongSelf.updateItemNodesHighlightedStates(animated: true) - } - - contextMenuController.dismissed = { - if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - if controllerInteraction.highlightedState?.messageStableId == message.stableId { - controllerInteraction.highlightedState = nil - strongSelf.updateItemNodesHighlightedStates(animated: true) + let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction).start(next: { contextMenuController in + if let strongSelf = self, let contextMenuController = contextMenuController { + if let controllerInteraction = strongSelf.controllerInteraction { + controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) + strongSelf.updateItemNodesHighlightedStates(animated: true) + } + + contextMenuController.dismissed = { + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { + if controllerInteraction.highlightedState?.messageStableId == message.stableId { + controllerInteraction.highlightedState = nil + strongSelf.updateItemNodesHighlightedStates(animated: true) + } } } + + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in + if let node = node { + return (node, frame) + } else { + return nil + } + })) } - - strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in - if let node = node { - return (node, frame) - } else { - return nil - } - })) - } + }) } } }, navigateToMessage: { [weak self] fromId, id in @@ -613,7 +620,7 @@ public class ChatController: TelegramController { } } } - }) + }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction @@ -659,14 +666,20 @@ public class ChatController: TelegramController { self.peerView.set(account.viewTracker.peerView(peerId)) - peerDisposable.set((self.peerView.get() + self.peerDisposable.set((self.peerView.get() |> deliverOnMainQueue).start(next: { [weak self] peerView in if let strongSelf = self { if let peer = peerViewMainPeer(peerView) { strongSelf.chatTitleView?.peerView = peerView (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] } }) + var peerIsMuted = false + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + peerIsMuted = true + } + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] }.updatedPeerIsMuted(peerIsMuted) }) if !strongSelf.didSetPeerReady { strongSelf.didSetPeerReady = true strongSelf._peerReady.set(.single(true)) @@ -674,7 +687,7 @@ public class ChatController: TelegramController { } })) - botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() + self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() |> deliverOnMainQueue).start(next: { [weak self] message in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { @@ -770,6 +783,17 @@ public class ChatController: TelegramController { } } }) + + self.automaticMediaDownloadSettingsDisposable = (account.telegramApplicationContext.automaticMediaDownloadSettings + |> deliverOnMainQueue).start(next: { [weak self] downloadSettings in + if let strongSelf = self, strongSelf.automaticMediaDownloadSettings != downloadSettings { + strongSelf.automaticMediaDownloadSettings = downloadSettings + strongSelf.controllerInteraction?.automaticMediaDownloadSettings = downloadSettings + if strongSelf.isNodeLoaded { + strongSelf.chatDisplayNode.updateAutomaticMediaDownloadSettings() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -797,6 +821,7 @@ public class ChatController: TelegramController { self.cachedDataDisposable?.dispose() self.resolveUrlDisposable?.dispose() self.chatUnreadCountDisposable?.dispose() + self.chatUnreadMentionCountDisposable?.dispose() self.peerInputActivitiesDisposable?.dispose() self.recentlyUsedInlineBotsDisposable?.dispose() self.unpinMessageDisposable?.dispose() @@ -818,7 +843,7 @@ public class ChatController: TelegramController { } override public func loadDisplayNode() { - self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, navigationBar: self.navigationBar!) + self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar!) let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) @@ -961,7 +986,7 @@ public class ChatController: TelegramController { return peerReady }) - self.chatDisplayNode.historyNode.visibleContentOffsetChanged = { [weak self] offset in + self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in if let strongSelf = self { let offsetAlpha: CGFloat switch offset { @@ -977,11 +1002,7 @@ public class ChatController: TelegramController { offsetAlpha = 0.0 } - if !strongSelf.chatDisplayNode.navigateToLatestButton.alpha.isEqual(to: offsetAlpha) { - UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: { - strongSelf.chatDisplayNode.navigateToLatestButton.alpha = offsetAlpha - }, completion: nil) - } + strongSelf.chatDisplayNode.navigateButtons.displayDownButton = !offsetAlpha.isZero } } @@ -1066,29 +1087,23 @@ public class ChatController: TelegramController { } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in - if let strongSelf = self { + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { if true { strongSelf.chatDisplayNode.dismissInput() - let emptyController = LegacyEmptyController() + let legacyController = LegacyController(presentation: .custom) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) - let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) - - var presentOverlayController: ((UIViewController) -> (() -> Void))? - let controller = legacyAttachmentMenu(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, presentOverlayController: { controller in - if let presentOverlayController = presentOverlayController { - return presentOverlayController(controller) - } else { - return { - } - } - }, openGallery: { + let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { self?.presentMediaPicker(fileMode: false) }, openCamera: { cameraView, menuController in - if let strongSelf = self { - presentedLegacyCamera(cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { + presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in self?.enqueueMediaMessages(signals: signals) }) } @@ -1133,27 +1148,16 @@ public class ChatController: TelegramController { }) } }) - controller.applicationInterface = legacyController.applicationInterface controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() } + controller.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } strongSelf.present(legacyController, in: .window(.root)) controller.present(in: emptyController, sourceView: nil, animated: true) - presentOverlayController = { [weak legacyController] controller in - if let legacyController = legacyController { - let childController = LegacyController(legacyController: controller, presentation: .custom) - legacyController.present(childController, in: .window(.root)) - return { [weak childController] in - childController?.dismiss() - } - } else { - return { - } - } - } - return } } @@ -1177,7 +1181,7 @@ public class ChatController: TelegramController { } } - self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in + 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) @@ -1187,6 +1191,17 @@ public class ChatController: TelegramController { } } + self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in + if let strongSelf = self, strongSelf.isNodeLoaded { + let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId) + strongSelf.navigationActionDisposable.set((signal |> take(1) |> deliverOnMainQueue).start(next: { messageId in + if let strongSelf = self, let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: messageId) + } + })) + } + } + let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { @@ -1468,8 +1483,10 @@ public class ChatController: TelegramController { self?.navigateToMessage(from: nil, to: messageId) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) - }, togglePeerNotifications: { - + }, togglePeerNotifications: { [weak self] in + if let strongSelf = self { + let _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() + } }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) }, sendBotCommand: { [weak self] botPeer, command in @@ -1600,6 +1617,34 @@ public class ChatController: TelegramController { self?.dismissReportPeer() }, deleteChat: { [weak self] in self?.deleteChat(reportChatSpam: false) + }, beginCall: { [weak self] in + if let strongSelf = self { + strongSelf.controllerInteraction?.callPeer(strongSelf.peerId) + } + }, toggleMessageStickerStarred: { [weak self] messageId in + if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + var stickerFile: TelegramMediaFile? + for media in message.media { + if let file = media as? TelegramMediaFile, file.isSticker { + stickerFile = file + } + } + if let stickerFile = stickerFile { + let postbox = strongSelf.account.postbox + let network = strongSelf.account.network + let _ = (strongSelf.account.postbox.modify { modifier -> Signal in + if getIsStickerSaved(modifier: modifier, fileId: stickerFile.fileId) { + removeSavedSticker(modifier: modifier, mediaId: stickerFile.fileId) + return .complete() + } else { + return addSavedSticker(postbox: postbox, network: network, file: stickerFile) + |> `catch` { _ -> Signal in + return .complete() + } + } + } |> switchToLatest).start() + } + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in @@ -1608,11 +1653,14 @@ public class ChatController: TelegramController { if let count = items.count(for: .peer(strongSelf.peerId)) { unreadCount = count } - if unreadCount != 0 { - strongSelf.chatDisplayNode.navigateToLatestButton.badge = "\(unreadCount)" - } else { - strongSelf.chatDisplayNode.navigateToLatestButton.badge = "" - } + + strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount + } + }) + + self.chatUnreadMentionCountDisposable = (self.account.viewTracker.unseenPersonalMessagesCount(peerId: self.peerId) |> deliverOnMainQueue).start(next: { [weak self] count in + if let strongSelf = self { + strongSelf.chatDisplayNode.navigateButtons.mentionCount = count } }) @@ -1911,43 +1959,30 @@ public class ChatController: TelegramController { } private func presentMediaPicker(fileMode: Bool) { - let _ = legacyAssetPicker(fileMode: fileMode).start(next: { [weak self] generator in - if let strongSelf = self { - var presentOverlayController: ((UIViewController) -> (() -> Void))? - let controller = generator({ controller in - return presentOverlayController!(controller) - }) - let legacyController = LegacyController(legacyController: controller, presentation: .modal(animateIn: true)) - - presentOverlayController = { [weak legacyController] controller in - if let legacyController = legacyController { - let childController = LegacyController(legacyController: controller, presentation: .custom) - legacyController.present(childController, in: .window(.root)) - return { [weak childController] in - childController?.dismiss() - } - } else { - return { + if let peer = self.presentationInterfaceState.peer { + let _ = legacyAssetPicker(fileMode: fileMode, peer: peer).start(next: { [weak self] generator in + if let strongSelf = self { + let legacyController = LegacyController(presentation: .modal(animateIn: true)) + let controller = generator(legacyController.context) + legacyController.bind(controller: controller) + + configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak self, weak legacyController] signals in + if let strongSelf = self, let legacyController = legacyController { + legacyController.dismiss() + strongSelf.enqueueMediaMessages(signals: signals) } } - } - - configureLegacyAssetPicker(controller) - controller.descriptionGenerator = legacyAssetPickerItemGenerator() - controller.completionBlock = { [weak self, weak legacyController] signals in - if let strongSelf = self, let legacyController = legacyController { - legacyController.dismiss() - strongSelf.enqueueMediaMessages(signals: signals) + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } } + strongSelf.present(legacyController, in: .window(.root)) } - controller.dismissalBlock = { [weak legacyController] in - if let legacyController = legacyController { - legacyController.dismiss() - } - } - strongSelf.present(legacyController, in: .window(.root)) - } - }) + }) + } } private func presentMapPicker() { @@ -2067,7 +2102,7 @@ public class ChatController: TelegramController { self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message)) } else { self.loadingMessage.set(true) - let historyView = chatHistoryViewForLocation(.InitialSearch(messageId: toId, count: 50), account: self.account, peerId: self.peerId, fixedCombinedReadState: nil, tagMask: nil, additionalData: []) + let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: 50), account: self.account, peerId: self.peerId, fixedCombinedReadState: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in switch historyView { @@ -2246,7 +2281,7 @@ public class ChatController: TelegramController { let title: String if let _ = peer as? TelegramGroup { title = self.presentationData.strings.Conversation_ReportSpamAndLeave - } else if let peer = peer as? TelegramChannel { + } else if let _ = peer as? TelegramChannel { title = self.presentationData.strings.Conversation_ReportSpamAndLeave } else { title = self.presentationData.strings.Conversation_ReportSpamAndLeave diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 347fdf3b91..e8a473e2ab 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -45,9 +45,6 @@ public final class ChatControllerInteraction { let openMessageContextMenu: (MessageId, ASDisplayNode, CGRect) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let clickThroughMessage: () -> Void - var hiddenMedia: [MessageId: [Media]] = [:] - var selectionState: ChatInterfaceSelectionState? - var highlightedState: ChatInterfaceHighlightedState? let toggleMessageSelection: (MessageId) -> Void let sendMessage: (String) -> Void let sendSticker: (TelegramMediaFile) -> Void @@ -66,7 +63,12 @@ public final class ChatControllerInteraction { let longTap: (ChatControllerInteractionLongTapAction) -> Void let openCheckoutOrReceipt: (MessageId) -> Void - public init(openMessage: @escaping (MessageId) -> Void, 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, toggleMessageSelection: @escaping (MessageId) -> 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) { + var hiddenMedia: [MessageId: [Media]] = [:] + var selectionState: ChatInterfaceSelectionState? + var highlightedState: ChatInterfaceHighlightedState? + var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + + public init(openMessage: @escaping (MessageId) -> Void, 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, toggleMessageSelection: @escaping (MessageId) -> 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, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -92,5 +94,7 @@ public final class ChatControllerInteraction { self.callPeer = callPeer self.longTap = longTap self.openCheckoutOrReceipt = openCheckoutOrReceipt + + self.automaticMediaDownloadSettings = automaticMediaDownloadSettings } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 495550c2ef..1235008417 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -39,11 +39,12 @@ class ChatControllerNode: ASDisplayNode { private var textInputPanelNode: ChatTextInputPanelNode? private var inputMediaNode: ChatMediaInputNode? - let navigateToLatestButton: ChatHistoryNavigationButtonNode + let navigateButtons: ChatHistoryNavigationButtons private var ignoreUpdateHeight = false var chatPresentationInterfaceState: ChatPresentationInterfaceState + var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } @@ -59,11 +60,12 @@ class ChatControllerNode: ASDisplayNode { private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? - init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, navigationBar: NavigationBar) { + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar) { self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction self.chatPresentationInterfaceState = chatPresentationInterfaceState + self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.navigationBar = navigationBar self.backgroundNode = ASDisplayNode() @@ -85,12 +87,13 @@ class ChatControllerNode: ASDisplayNode { self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelStrokeColor self.inputPanelBackgroundSeparatorNode.isLayerBacked = true - self.navigateToLatestButton = ChatHistoryNavigationButtonNode(theme: self.chatPresentationInterfaceState.theme) - self.navigateToLatestButton.alpha = 0.0 + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor(rgb: 0xdee3e9) @@ -103,7 +106,9 @@ class ChatControllerNode: ASDisplayNode { } else { switch wallpaper { case .builtin: - backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0")?.precomposed() + 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) @@ -123,17 +128,14 @@ class ChatControllerNode: ASDisplayNode { self.backgroundNode.contents = backgroundImage?.cgImage - self.addSubnode(self.backgroundNode) - self.addSubnode(self.historyNode) - self.addSubnode(self.titleAccessoryPanelContainer) self.addSubnode(self.inputPanelBackgroundNode) self.addSubnode(self.inputPanelBackgroundSeparatorNode) - self.addSubnode(self.navigateToLatestButton) + self.addSubnode(self.navigateButtons) self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) @@ -295,7 +297,7 @@ class ChatControllerNode: ASDisplayNode { } insets.top += navigationBarHeight - transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 50.0))) + transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) var titleAccessoryPanelFrame: CGRect? if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { @@ -347,7 +349,7 @@ class ChatControllerNode: ASDisplayNode { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode - self.insertSubnode(inputPanelNode, belowSubnode: self.navigateToLatestButton) + self.insertSubnode(inputPanelNode, aboveSubnode: self.navigateButtons) } else { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, transition: transition, interfaceState: self.chatPresentationInterfaceState) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) @@ -369,7 +371,7 @@ class ChatControllerNode: ASDisplayNode { if let inputPanelNode = self.inputPanelNode { self.insertSubnode(accessoryPanelNode, belowSubnode: inputPanelNode) } else { - self.insertSubnode(accessoryPanelNode, belowSubnode: self.navigateToLatestButton) + self.insertSubnode(accessoryPanelNode, aboveSubnode: self.navigateButtons) } accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in @@ -428,12 +430,12 @@ class ChatControllerNode: ASDisplayNode { listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.bottom + inputPanelsHeight + 4.0, left: insets.right, bottom: insets.top, right: insets.left), duration: duration, curve: listViewCurve)) - let navigateToLatestButtonSize = self.navigateToLatestButton.bounds.size - let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize) + let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) + let navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateButtonsSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame) + transition.updateFrame(node: self.navigateButtons, frame: navigateButtonsFrame) if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { if immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance { @@ -607,7 +609,7 @@ class ChatControllerNode: ASDisplayNode { let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState - self.navigateToLatestButton.updateTheme(theme: chatPresentationInterfaceState.theme) + self.navigateButtons.updateTheme(theme: chatPresentationInterfaceState.theme) let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil var extendedSearchLayout = false @@ -667,6 +669,14 @@ class ChatControllerNode: ASDisplayNode { } } + func updateAutomaticMediaDownloadSettings() { + self.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateAutomaticMediaDownloadSettings() + } + } + } + func ensureInputViewFocused() { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputPanelNode.ensureFocused() diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index a99efe9640..71a5a239b3 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -68,6 +68,8 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { if #available(iOS 9.0, *) { let webView = WKWebView() + webView.allowsLinkPreview = false + webView.allowsBackForwardNavigationGestures = false self.webView = webView } else { let webView = UIWebView() diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 456c040dd7..88e64adddd 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -36,15 +36,15 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { case let .MessageEntry(message, _, _, _, _): - return GridNodeUpdateItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) case .HoleEntry: - return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .UnreadEntry: assertionFailure() - return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .ChatInfoEntry, .EmptyChatInfoEntry: assertionFailure() - return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) } } } @@ -250,7 +250,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { self.historyDisposable.set(appliedTransition.start()) if let messageId = messageId { - self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 100)) + self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(location: .id(messageId), count: 100)) } else { self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: 100)) } @@ -382,15 +382,15 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: CGSize(width: 200.0, height: 200.0), lineSpacing: 0.0)), transition: .immediate) } - self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: mappedTransition.topOffsetWithinMonth), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: mappedTransition.topOffsetWithinMonth), completion: completion) } else { - self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.topOffsetWithinMonth), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.topOffsetWithinMonth), completion: completion) } } } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { - self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 68c8daa9c9..e5859838d7 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -178,6 +178,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let account: Account private let peerId: PeerId private let messageId: MessageId? + private let tagMask: MessageTags? private let controllerInteraction: ChatControllerInteraction private let mode: ChatHistoryListMode @@ -214,6 +215,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) let canReadHistory = Promise() + private var canReadHistoryValue: Bool = false + private var canReadHistoryDisposable: Disposable? private let _chatHistoryLocation = ValuePromise() private var chatHistoryLocation: Signal { @@ -223,6 +226,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() + private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? @@ -233,10 +237,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var themeAndStrings: Promise<(PresentationTheme, PresentationStrings)> private var presentationDataDisposable: Disposable? + private var isScrollAtBottomPosition = false + private var interactiveReadActionDisposable: Disposable? + + public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in } + public init(account: Account, peerId: PeerId, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode = .bubbles) { self.account = account self.peerId = peerId self.messageId = messageId + self.tagMask = tagMask self.controllerInteraction = controllerInteraction self.mode = mode @@ -253,6 +263,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.messageProcessingManager.process = { [weak account] messageIds in account?.viewTracker.updateViewCountForMessageIds(messageIds: messageIds) } + self.messageMentionProcessingManager.process = { [weak account] messageIds in + account?.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + } self.preloadPages = false switch self.mode { @@ -368,8 +381,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.readHistoryDisposable.set(readHistory.start()) + self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if strongSelf.canReadHistoryValue != value { + strongSelf.canReadHistoryValue = value + strongSelf.updateReadHistoryActions() + } + } + }) + if let messageId = messageId { - self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 60)) + self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(location: .id(messageId), count: 60)) } else { self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: 60)) } @@ -381,8 +403,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) var messageIdsWithViewCount: [MessageId] = [] + var messageIdsWithUnseenPersonalMention: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { if case let .MessageEntry(message, _, _, _, _) = historyView.filteredEntries[i] { + if message.tags.contains(.unseenPersonalMessage) { + for attribute in message.attributes { + if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { + messageIdsWithUnseenPersonalMention.append(message.id) + } + } + } inner: for attribute in message.attributes { if attribute is ViewCountMessageAttribute { messageIdsWithViewCount.append(message.id) @@ -396,6 +426,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.messageProcessingManager.add(messageIdsWithViewCount) } + if !messageIdsWithUnseenPersonalMention.isEmpty { + strongSelf.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) + } + let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView.filteredEntries, indexRange: indexRange) if let maxIncomingIndex = maxIncomingIndex { @@ -437,11 +471,36 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } }) + + self.visibleContentOffsetChanged = { [weak self] offset in + if let strongSelf = self { + strongSelf.contentPositionChanged(offset) + + if strongSelf.tagMask == nil { + var atBottom = false + switch offset { + case let .known(offsetValue): + if offsetValue.isLessThanOrEqualTo(0.0) { + atBottom = true + } + default: + break + } + + if atBottom != strongSelf.isScrollAtBottomPosition { + strongSelf.isScrollAtBottomPosition = atBottom + strongSelf.updateReadHistoryActions() + } + } + } + } } deinit { self.historyDisposable.dispose() self.readHistoryDisposable.dispose() + self.interactiveReadActionDisposable?.dispose() + self.canReadHistoryDisposable?.dispose() } public func scrollToStartOfHistory() { @@ -602,4 +661,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func disconnect() { self.historyDisposable.set(nil) } + + private func updateReadHistoryActions() { + let canRead = self.canReadHistoryValue && self.isScrollAtBottomPosition + if canRead != (self.interactiveReadActionDisposable != nil) { + if let interactiveReadActionDisposable = self.interactiveReadActionDisposable { + if !canRead { + interactiveReadActionDisposable.dispose() + self.interactiveReadActionDisposable = nil + } + } else if self.interactiveReadActionDisposable == nil { + self.interactiveReadActionDisposable = installInteractiveReadMessagesAction(postbox: self.account.postbox, peerId: self.peerId) + } + } + } } diff --git a/TelegramUI/ChatHistoryLocation.swift b/TelegramUI/ChatHistoryLocation.swift index 8682418630..3b6516e384 100644 --- a/TelegramUI/ChatHistoryLocation.swift +++ b/TelegramUI/ChatHistoryLocation.swift @@ -1,9 +1,14 @@ import Postbox import Display +enum ChatHistoryInitialSearchLocation { + case index(MessageIndex) + case id(MessageId) +} + enum ChatHistoryLocation: Equatable { case Initial(count: Int) - case InitialSearch(messageId: MessageId, count: Int) + case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int) case Navigation(index: MessageIndex, anchorIndex: MessageIndex) case Scroll(index: MessageIndex, anchorIndex: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool) } diff --git a/TelegramUI/ChatHistoryNavigationButtonNode.swift b/TelegramUI/ChatHistoryNavigationButtonNode.swift index b1204de4bd..4daf40e838 100644 --- a/TelegramUI/ChatHistoryNavigationButtonNode.swift +++ b/TelegramUI/ChatHistoryNavigationButtonNode.swift @@ -4,6 +4,11 @@ import Display private let badgeFont = Font.regular(13.0) +enum ChatHistoryNavigationButtonType { + case down + case mentions +} + class ChatHistoryNavigationButtonNode: ASControlNode { private let imageNode: ASImageNode private let badgeBackgroundNode: ASImageNode @@ -21,12 +26,17 @@ class ChatHistoryNavigationButtonNode: ASControlNode { private var theme: PresentationTheme - init(theme: PresentationTheme) { + init(theme: PresentationTheme, type: ChatHistoryNavigationButtonType) { self.theme = theme self.imageNode = ASImageNode() self.imageNode.displayWithoutProcessing = true - self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + switch type { + case .down: + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + case .mentions: + self.imageNode.image = PresentationResourcesChat.chatHistoryMentionsButtonImage(theme) + } self.imageNode.isLayerBacked = true self.badgeBackgroundNode = ASImageNode() diff --git a/TelegramUI/ChatHistoryNavigationButtons.swift b/TelegramUI/ChatHistoryNavigationButtons.swift new file mode 100644 index 0000000000..2cd0b206b6 --- /dev/null +++ b/TelegramUI/ChatHistoryNavigationButtons.swift @@ -0,0 +1,106 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class ChatHistoryNavigationButtons: ASDisplayNode { + private var theme: PresentationTheme + + private let mentionsButton: ChatHistoryNavigationButtonNode + private let downButton: ChatHistoryNavigationButtonNode + + var downPressed: (() -> Void)? { + didSet { + self.downButton.tapped = self.downPressed + } + } + + var mentionsPressed: (() -> Void)? { + didSet { + self.mentionsButton.tapped = self.mentionsPressed + } + } + + var displayDownButton: Bool = false { + didSet { + if oldValue != self.displayDownButton { + let _ = self.updateLayout(transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + + var unreadCount: Int32 = 0 { + didSet { + if self.unreadCount != 0 { + self.downButton.badge = "\(self.unreadCount)" + } else { + self.downButton.badge = "" + } + } + } + + var mentionCount: Int32 = 0 { + didSet { + if self.mentionCount != 0 { + self.mentionsButton.badge = "\(self.mentionCount)" + } else { + self.mentionsButton.badge = "" + } + + if (oldValue != 0) != (self.mentionCount != 0) { + let _ = self.updateLayout(transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + + init(theme: PresentationTheme) { + self.theme = theme + + self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .mentions) + self.mentionsButton.alpha = 0.0 + self.downButton = ChatHistoryNavigationButtonNode(theme: theme, type: .down) + self.downButton.alpha = 0.0 + + super.init() + + self.addSubnode(self.mentionsButton) + self.addSubnode(self.downButton) + } + + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.mentionsButton.updateTheme(theme: theme) + self.downButton.updateTheme(theme: theme) + } + } + + func updateLayout(transition: ContainedViewLayoutTransition) -> CGSize { + let buttonSize = CGSize(width: 38.0, height: 38.0) + let completeSize = CGSize(width: buttonSize.width, height: buttonSize.height * 2.0 + 6.0) + var mentionsOffset: CGFloat = 0.0 + + if self.displayDownButton { + mentionsOffset = buttonSize.height + 8.0 + transition.updateAlpha(node: self.downButton, alpha: 1.0) + transition.updateTransformScale(node: self.downButton, scale: 1.0) + } else { + transition.updateAlpha(node: self.downButton, alpha: 0.0) + transition.updateTransformScale(node: self.downButton, scale: 0.2) + } + + if self.mentionCount != 0 { + transition.updateAlpha(node: self.mentionsButton, alpha: 1.0) + transition.updateTransformScale(node: self.mentionsButton, scale: 1.0) + } else { + transition.updateAlpha(node: self.mentionsButton, alpha: 0.0) + transition.updateTransformScale(node: self.mentionsButton, scale: 0.2) + } + + transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) + + transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) + + return completeSize + } +} diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 899fa17c68..32faf70ac5 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -83,10 +83,19 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } } - case let .InitialSearch(messageId, count): + case let .InitialSearch(searchLocation, count): var preloaded = false var fadeIn = false - return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + + let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> + switch searchLocation { + case let .index(index): + signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: count, anchorIndex: index, fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + case let .id(id): + signal = account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: id, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + } + + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? for data in view.additionalData { if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { @@ -118,7 +127,6 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - //case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData)) } } diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index 00502c748c..60934a9471 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -9,6 +9,8 @@ private enum ChatInfoTitleButton { case info case mute case unmute + case call + case report func title(_ strings: PresentationStrings) -> String { switch self { @@ -20,15 +22,79 @@ private enum ChatInfoTitleButton { return strings.Conversation_Mute case .unmute: return strings.Conversation_Unmute + case .call: + return strings.Conversation_Call + case .report: + return strings.ReportPeer_Report + } + } + + func icon(_ theme: PresentationTheme) -> UIImage? { + switch self { + case .search: + return PresentationResourcesChat.chatTitlePanelSearchImage(theme) + case .info: + return PresentationResourcesChat.chatTitlePanelInfoImage(theme) + case .mute: + return PresentationResourcesChat.chatTitlePanelMuteImage(theme) + case .unmute: + return PresentationResourcesChat.chatTitlePanelUnmuteImage(theme) + case .call: + return PresentationResourcesChat.chatTitlePanelCallImage(theme) + case .report: + return PresentationResourcesChat.chatTitlePanelReportImage(theme) } } } -private func peerButtons(_ peer: Peer) -> [ChatInfoTitleButton] { - if let _ = peer as? TelegramUser { - return [.search, .info] +private func peerButtons(_ peer: Peer, isMuted: Bool) -> [ChatInfoTitleButton] { + let muteAction: ChatInfoTitleButton + if isMuted { + muteAction = .unmute } else { - return [.search, .mute] + muteAction = .mute + } + + if let _ = peer as? TelegramUser { + return [.search, muteAction, .call, .info] + } else if let channel = peer as? TelegramChannel { + if channel.flags.contains(.isCreator) { + return [.search, muteAction, .info] + } else { + return [.search, .report, muteAction, .info] + } + } else if let group = peer as? TelegramGroup { + if case .creator = group.role { + return [.search, muteAction, .info] + } else { + return [.search, .report, muteAction, .info] + } + } else { + return [.search, muteAction, .info] + } +} + +private let buttonFont = Font.regular(10.0) + +private final class ChatInfoTitlePanelButtonNode: HighlightableButtonNode { + override init() { + super.init() + + self.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.displaysAsynchronously = false + + self.titleNode.displaysAsynchronously = false + + self.laysOutHorizontally = false + } + + func setup(text: String, color: UIColor, icon: UIImage?) { + self.setTitle(text, with: buttonFont, with: color, for: []) + self.setImage(icon, for: []) + if let icon = icon { + self.contentSpacing = max(0.0, 32.0 - icon.size.height) + } } } @@ -36,7 +102,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? private let separatorNode: ASDisplayNode - private var buttons: [(ChatInfoTitleButton, UIButton)] = [] + private var buttons: [(ChatInfoTitleButton, ChatInfoTitlePanelButtonNode)] = [] override init() { self.separatorNode = ASDisplayNode() @@ -51,7 +117,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { let themeUpdated = self.theme !== interfaceState.theme self.theme = interfaceState.theme - let panelHeight: CGFloat = 44.0 + let panelHeight: CGFloat = 55.0 if themeUpdated { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor @@ -60,7 +126,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { let updatedButtons: [ChatInfoTitleButton] if let peer = interfaceState.peer { - updatedButtons = peerButtons(peer) + updatedButtons = peerButtons(peer, isMuted: interfaceState.peerIsMuted) } else { updatedButtons = [] } @@ -78,27 +144,27 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { } if buttonsUpdated || themeUpdated { - for (_, view) in self.buttons { - view.removeFromSuperview() + for (_, buttonNode) in self.buttons { + buttonNode.removeFromSupernode() } self.buttons.removeAll() for button in updatedButtons { - let view = UIButton() - view.setTitle(button.title(interfaceState.strings), for: []) - view.titleLabel?.font = Font.regular(17.0) - view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor, for: []) - view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7), for: [.highlighted]) - view.addTarget(self, action: #selector(self.buttonPressed(_:)), for: [.touchUpInside]) - self.view.addSubview(view) - self.buttons.append((button, view)) + let buttonNode = ChatInfoTitlePanelButtonNode() + buttonNode.laysOutHorizontally = false + + buttonNode.setup(text: button.title(interfaceState.strings), color: interfaceState.theme.rootController.navigationBar.accentTextColor, icon: button.icon(interfaceState.theme)) + + buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside]) + self.addSubnode(buttonNode) + self.buttons.append((button, buttonNode)) } } if !self.buttons.isEmpty { let buttonWidth = floor(width / CGFloat(self.buttons.count)) var nextButtonOrigin: CGFloat = 0.0 - for (_, view) in self.buttons { - view.frame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) + for (_, buttonNode) in self.buttons { + buttonNode.frame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) nextButtonOrigin += buttonWidth } } @@ -108,9 +174,9 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { return panelHeight } - @objc func buttonPressed(_ view: UIButton) { - for (button, buttonView) in self.buttons { - if buttonView === view { + @objc func buttonPressed(_ node: HighlightableButtonNode) { + for (button, buttonNode) in self.buttons { + if buttonNode === node { switch button { case .info: self.interfaceInteraction?.openPeerInfo() @@ -120,6 +186,10 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { self.interfaceInteraction?.togglePeerNotifications() case .search: self.interfaceInteraction?.beginMessageSearch() + case .call: + self.interfaceInteraction?.beginCall() + case .report: + self.interfaceInteraction?.reportPeer() } break } diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index 9335742198..4645f78e2e 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -2,7 +2,7 @@ import Foundation import TelegramCore func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { - guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResult, let peer = chatPresentationInterfaceState.peer else { + guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResult, let _ = chatPresentationInterfaceState.peer else { return nil } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index c841bcf2f0..20eb73f524 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -5,12 +5,33 @@ import Display import UIKit import SwiftSignalKit -func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ContextMenuController? { +private struct MessageContextMenuData { + let starStatus: Bool? + let canReply: Bool + let canPin: Bool + let canEdit: Bool +} + +private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIconEmpty")?.precomposed() +private let starIconFilled = UIImage(bundleImageName: "Chat/Context Menu/StarIconFilled")?.precomposed() + +func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal { guard let peer = chatPresentationInterfaceState.peer, let interfaceInteraction = interfaceInteraction else { - return nil + return .single(nil) } - var actions: [ContextMenuAction] = [] + let dataSignal: Signal + + var loadStickerSaveStatus: MediaId? + for media in message.media { + if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + if case let .Sticker(_, packInfo, _) = attribute, packInfo != nil { + loadStickerSaveStatus = file.fileId + } + } + } + } var canReply = false var canPin = false @@ -44,57 +65,83 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat } } - if canReply { - actions.append(ContextMenuAction(content: .text("Reply"), action: { - interfaceInteraction.setupReplyMessage(message.id) - })) - } - - if canEdit { - actions.append(ContextMenuAction(content: .text("Edit"), action: { - interfaceInteraction.setupEditMessage(message.id) - })) - } - - actions.append(ContextMenuAction(content: .text("Copy"), action: { - if !message.text.isEmpty { - UIPasteboard.general.string = message.text + if loadStickerSaveStatus != nil { + dataSignal = account.postbox.modify { modifier -> MessageContextMenuData in + var starStatus: Bool? + if let loadStickerSaveStatus = loadStickerSaveStatus { + if getIsStickerSaved(modifier: modifier, fileId: loadStickerSaveStatus) { + starStatus = true + } else { + starStatus = false + } + } + + return MessageContextMenuData(starStatus: starStatus, canReply: canReply, canPin: canPin, canEdit: canEdit) } - })) + } else { + dataSignal = .single(MessageContextMenuData(starStatus: nil, canReply: canReply, canPin: canPin, canEdit: canEdit)) + } - if canPin { - if chatPresentationInterfaceState.pinnedMessageId != message.id { - actions.append(ContextMenuAction(content: .text("Pin"), action: { - interfaceInteraction.pinMessage(message.id) - })) - } else { - actions.append(ContextMenuAction(content: .text("Unpin"), action: { - interfaceInteraction.unpinMessage() + return dataSignal |> deliverOnMainQueue |> map { data -> ContextMenuController? in + var actions: [ContextMenuAction] = [] + + if let starStatus = data.starStatus, let image = starStatus ? starIconFilled : starIconEmpty { + actions.append(ContextMenuAction(content: .icon(image), action: { + interfaceInteraction.toggleMessageStickerStarred(message.id) })) } - } - - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVideo && file.isAnimated { - actions.append(ContextMenuAction(content: .text("Save"), action: { - let _ = addSavedGif(postbox: account.postbox, file: file).start() + + if data.canReply { + actions.append(ContextMenuAction(content: .text("Reply"), action: { + interfaceInteraction.setupReplyMessage(message.id) + })) + } + + if data.canEdit { + actions.append(ContextMenuAction(content: .text("Edit"), action: { + interfaceInteraction.setupEditMessage(message.id) + })) + } + + actions.append(ContextMenuAction(content: .text("Copy"), action: { + if !message.text.isEmpty { + UIPasteboard.general.string = message.text + } + })) + + if data.canPin { + if chatPresentationInterfaceState.pinnedMessageId != message.id { + actions.append(ContextMenuAction(content: .text("Pin"), action: { + interfaceInteraction.pinMessage(message.id) + })) + } else { + actions.append(ContextMenuAction(content: .text("Unpin"), action: { + interfaceInteraction.unpinMessage() })) - break } } - } - - actions.append(ContextMenuAction(content: .text("More..."), action: { - interfaceInteraction.beginMessageSelection(message.id) - })) - - - if !actions.isEmpty { - let contextMenuController = ContextMenuController(actions: actions) - return contextMenuController - } else { - return nil + + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isVideo && file.isAnimated { + actions.append(ContextMenuAction(content: .text("Save"), action: { + let _ = addSavedGif(postbox: account.postbox, file: file).start() + })) + break + } + } + } + + actions.append(ContextMenuAction(content: .text("More..."), action: { + interfaceInteraction.beginMessageSelection(message.id) + })) + + if !actions.isEmpty { + let contextMenuController = ContextMenuController(actions: actions) + return contextMenuController + } else { + return nil + } } } diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index 56d9321f09..918c4c6e04 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -22,7 +22,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = searchStickers(postbox: account.postbox, query: query) |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in - return .stickers(stickers) + return .stickers(stickers) } } return (inputQuery, signal |> then(stickers)) diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 1e1ea49438..865f551d06 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -45,6 +45,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected") + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) @@ -139,6 +140,17 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD private func updateThemeAndStrings() { self.tabBarItem.title = self.presentationData.strings.DialogList_Title + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) + var editing = false + self.chatListDisplayNode.chatListNode.updateState { state in + editing = state.editing + return state + } + if editing { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + } else { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) self.titleView.theme = self.presentationData.theme diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index f301be381c..9205efc5d9 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -26,9 +26,11 @@ class ChatListControllerNode: ASDisplayNode { self.themeAndStrings = (theme, strings) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.addSubnode(self.chatListNode) } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index b662f06937..56f845659b 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -15,6 +15,7 @@ class ChatListItem: ListViewItem { let peer: RenderedPeer let combinedReadState: CombinedPeerReadState? let notificationSettings: PeerNotificationSettings? + let summaryInfo: ChatListMessageTagSummaryInfo let embeddedState: PeerChatListEmbeddedInterfaceState? let editing: Bool let hasActiveRevealControls: Bool @@ -24,7 +25,7 @@ class ChatListItem: ListViewItem { let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, index: ChatListIndex, message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedState: PeerChatListEmbeddedInterfaceState?, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, index: ChatListIndex, message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { self.theme = theme self.strings = strings self.account = account @@ -33,6 +34,7 @@ class ChatListItem: ListViewItem { self.peer = peer self.combinedReadState = combinedReadState self.notificationSettings = notificationSettings + self.summaryInfo = summaryInfo self.embeddedState = embeddedState self.editing = editing self.hasActiveRevealControls = hasActiveRevealControls @@ -178,6 +180,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let separatorNode: ASDisplayNode let badgeBackgroundNode: ASImageNode let badgeTextNode: TextNode + let mentionBadgeNode: ASImageNode let mutedIconNode: ASImageNode var editableControlNode: ItemListEditableControlNode? @@ -229,6 +232,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.badgeBackgroundNode.displaysAsynchronously = false self.badgeBackgroundNode.displayWithoutProcessing = true + self.mentionBadgeNode = ASImageNode() + self.mentionBadgeNode.isLayerBacked = true + self.mentionBadgeNode.displaysAsynchronously = false + self.mentionBadgeNode.displayWithoutProcessing = true + self.badgeTextNode = TextNode() self.badgeTextNode.isLayerBacked = true self.badgeTextNode.displaysAsynchronously = true @@ -253,6 +261,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.dateNode) self.addSubnode(self.statusNode) self.addSubnode(self.badgeBackgroundNode) + self.addSubnode(self.mentionBadgeNode) self.addSubnode(self.badgeTextNode) self.addSubnode(self.mutedIconNode) } @@ -350,8 +359,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var statusImage: UIImage? var currentBadgeBackgroundImage: UIImage? + var currentMentionBadgeImage: UIImage? var currentMutedIconImage: UIImage? + let tagSummaryCount = item.summaryInfo.tagSummaryCount ?? 0 + let actionsSummaryCount = item.summaryInfo.actionsSummaryCount ?? 0 + let totalMentionCount = tagSummaryCount - actionsSummaryCount + if totalMentionCount > 0 { + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.theme) + } + var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? let editingOffset: CGFloat @@ -414,7 +431,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { messageText = descriptionString break inner } - case let .Sticker(displayText, _): + case let .Sticker(displayText, _, _): if displayText.isEmpty { messageText = item.strings.Message_Sticker break inner @@ -569,11 +586,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let badgeSize: CGFloat + var badgeSize: CGFloat = 0.0 if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { - badgeSize = max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 5.0 - } else { - badgeSize = 0.0 + badgeSize += max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 5.0 + } + if let currentMentionBadgeImage = currentMentionBadgeImage { + if !badgeSize.isZero { + badgeSize += currentMentionBadgeImage.size.width + 4.0 + } else { + badgeSize += currentMentionBadgeImage.size.width + 5.0 + } } let (authorLayout, authorApply) = authorLayout(hideAuthor ? nil : authorAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) @@ -658,21 +680,44 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.isHidden = true } + let badgeBackgroundWidth: CGFloat if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage strongSelf.badgeBackgroundNode.isHidden = false - let badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) + badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeLayout.size) strongSelf.badgeTextNode.frame = badgeTextFrame strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame } else { + badgeBackgroundWidth = 0.0 strongSelf.badgeBackgroundNode.image = nil strongSelf.badgeBackgroundNode.isHidden = true } + if let currentMentionBadgeImage = currentMentionBadgeImage { + strongSelf.mentionBadgeNode.image = currentMentionBadgeImage + strongSelf.mentionBadgeNode.isHidden = false + + let mentionBadgeSize = currentMentionBadgeImage.size + let mentionBadgeOffset: CGFloat + if badgeBackgroundWidth.isZero { + mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width + } else { + mentionBadgeOffset = contentRect.maxX - badgeBackgroundWidth - 6.0 - mentionBadgeSize.width + } + + let badgeBackgroundWidth = mentionBadgeSize.width + let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeSize.height - 2.0, width: badgeBackgroundWidth, height: mentionBadgeSize.height) + + strongSelf.mentionBadgeNode.frame = badgeBackgroundFrame + } else { + strongSelf.mentionBadgeNode.image = nil + strongSelf.mentionBadgeNode.isHidden = true + } + if let currentMutedIconImage = currentMutedIconImage { strongSelf.mutedIconNode.image = currentMutedIconImage strongSelf.mutedIconNode.isHidden = false diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 98c985807d..1acc03ec5a 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -82,10 +82,10 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? @@ -112,10 +112,10 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, summaryInfo: summaryInfo, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index 6a9efb6155..bf8eca9507 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -62,14 +62,14 @@ enum ChatListNodeEntryId: Hashable, CustomStringConvertible { enum ChatListNodeEntry: Comparable, Identifiable { case SearchEntry(theme: PresentationTheme, text: String) - case PeerEntry(index: ChatListIndex, theme: PresentationTheme, strings: PresentationStrings, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, editing: Bool, hasActiveRevealControls: Bool) + case PeerEntry(index: ChatListIndex, theme: PresentationTheme, strings: PresentationStrings, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool) case HoleEntry(ChatListHole, theme: PresentationTheme) var index: ChatListIndex { switch self { case .SearchEntry: return ChatListIndex.absoluteUpperBound - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole, _): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) @@ -80,7 +80,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { switch self { case .SearchEntry: return .Search - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _): return .PeerId(index.messageIndex.id.peerId.toInt64()) case let .HoleEntry(hole, _): return .Hole(Int64(hole.index.id.id)) @@ -99,9 +99,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } - case let .PeerEntry(lhsIndex, lhsTheme, lhsStrings, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsEditing, lhsHasRevealControls): + case let .PeerEntry(lhsIndex, lhsTheme, lhsStrings, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls): switch rhs { - case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsEditing, rhsHasRevealControls): + case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls): if lhsIndex != rhsIndex { return false } @@ -140,6 +140,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsPeer != rhsPeer { return false } + if lhsSummaryInfo != rhsSummaryInfo { + return false + } return true default: @@ -160,8 +163,8 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState) var result: [ChatListNodeEntry] = [] for entry in view.entries { switch entry { - case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer): - result.append(.PeerEntry(index: index, theme: state.theme, strings: state.strings, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions)) + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo): + result.append(.PeerEntry(index: index, theme: state.theme, strings: state.strings, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions)) case let .HoleEntry(hole): result.append(.HoleEntry(hole, theme: state.theme)) } diff --git a/TelegramUI/ChatListNodeLocation.swift b/TelegramUI/ChatListNodeLocation.swift index e4f5dabb94..dc2a8564b0 100644 --- a/TelegramUI/ChatListNodeLocation.swift +++ b/TelegramUI/ChatListNodeLocation.swift @@ -34,13 +34,13 @@ func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) switch location { case let .initial(count): let signal: Signal<(ChatListView, ViewUpdateType), NoError> - signal = account.postbox.tailChatListView(count) + signal = account.viewTracker.tailChatListView(count: count) return signal |> map { view, updateType -> ChatListNodeViewUpdate in return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil) } case let .navigation(index): var first = true - return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + return account.viewTracker.aroundChatListView(index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType if first { first = false @@ -54,7 +54,7 @@ func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + return account.viewTracker.aroundChatListView(index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil if first { diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 7a53f8303e..2c3a4802c6 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -257,7 +257,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { interaction.peerSelected(peer) }) case let .message(message, theme, strings): - return ChatListItem(theme: theme, strings: strings, account: account, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, embeddedState: nil, editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: theme, strings: strings) : nil, interaction: interaction) + return ChatListItem(theme: theme, strings: strings, account: account, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: theme, strings: strings) : nil, interaction: interaction) } } } diff --git a/TelegramUI/ChatMediaInputGridEntries.swift b/TelegramUI/ChatMediaInputGridEntries.swift index 138bbb1c9b..5d1b7dbd1c 100644 --- a/TelegramUI/ChatMediaInputGridEntries.swift +++ b/TelegramUI/ChatMediaInputGridEntries.swift @@ -27,7 +27,7 @@ struct ChatMediaInputGridEntry: Comparable, Identifiable { } static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { - return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem && lhs.theme === rhs.theme + return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem && lhs.stickerPackInfo?.id == rhs.stickerPackInfo?.id && lhs.theme === rhs.theme } static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { diff --git a/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift similarity index 59% rename from TelegramUI/ChatMediaInputRecentStickerPacksItem.swift rename to TelegramUI/ChatMediaInputMetaSectionItemNode.swift index 72d71c135f..e9b008585f 100644 --- a/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift +++ b/TelegramUI/ChatMediaInputMetaSectionItemNode.swift @@ -5,8 +5,14 @@ import TelegramCore import SwiftSignalKit import Postbox -final class ChatMediaInputRecentStickerPacksItem: ListViewItem { +enum ChatMediaInputMetaSectionItemType { + case savedStickers + case recentStickers +} + +final class ChatMediaInputMetaSectionItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction + let type: ChatMediaInputMetaSectionItemType let theme: PresentationTheme let selectedItem: () -> Void @@ -14,18 +20,20 @@ final class ChatMediaInputRecentStickerPacksItem: ListViewItem { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction + self.type = type self.selectedItem = selected self.theme = theme } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { - let node = ChatMediaInputRecentStickerPacksItemNode() + let node = ChatMediaInputMetaSectionItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction + node.setItem(item: self) node.updateTheme(theme: self.theme) completion(node, { return (nil, { @@ -37,7 +45,8 @@ final class ChatMediaInputRecentStickerPacksItem: ListViewItem { public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { - (node as? ChatMediaInputRecentStickerPacksItemNode)?.updateTheme(theme: self.theme) + (node as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self) + (node as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme) }) } @@ -51,10 +60,11 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { +final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { private let imageNode: ASImageNode private let highlightNode: ASImageNode + var item: ChatMediaInputMetaSectionItem? var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -77,13 +87,18 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { self.addSubnode(self.highlightNode) self.addSubnode(self.imageNode) - self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) - let imageSize = CGSize(width: 26.0, height: 26.0) self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) } - deinit { + func setItem(item: ChatMediaInputMetaSectionItem) { + self.item = item + switch item.type { + case .savedStickers: + self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) + case .recentStickers: + self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) + } } func updateTheme(theme: PresentationTheme) { @@ -91,7 +106,14 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { self.theme = theme self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) - self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + if let item = self.item { + switch item.type { + case .savedStickers: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme) + case .recentStickers: + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + } + } } } @@ -100,4 +122,19 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + self.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index e914d70a68..15beb4890f 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -18,6 +18,7 @@ private struct ChatMediaInputGridTransition { let updateFirstIndexInSectionOffset: Int? let stationaryItems: GridNodeStationaryItems let scrollToItem: GridNodeScrollToItem? + let animated: Bool } private func preparedChatMediaInputPanelEntryTransition(account: Account, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition { @@ -33,9 +34,10 @@ private func preparedChatMediaInputPanelEntryTransition(account: Account, from f private func preparedChatMediaInputGridEntryTransition(account: Account, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition { var stationaryItems: GridNodeStationaryItems = .none var scrollToItem: GridNodeScrollToItem? + var animated = false switch update { case .generic: - break + animated = true case .scroll: var fromStableIds = Set() for entry in fromEntries { @@ -71,19 +73,22 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) } var firstIndexInSectionOffset = 0 if !toEntries.isEmpty { firstIndexInSectionOffset = Int(toEntries[0].index.itemIndex.index) } - return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem) + return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) } -private func chatMediaInputPanelEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { +private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.recentGifs(theme)) + if let savedStickers = savedStickers, !savedStickers.items.isEmpty { + entries.append(.savedStickers(theme)) + } if let recentStickers = recentStickers, !recentStickers.items.isEmpty { entries.append(.recentPacks(theme)) } @@ -97,7 +102,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, recentSticker return entries } -private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { +private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] @@ -107,8 +112,19 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers } } + if let savedStickers = savedStickers, !savedStickers.items.isEmpty { + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_Favorited.uppercased(), shortName: "", hash: 0, count: 0) + for i in 0 ..< savedStickers.items.count { + if let item = savedStickers.items[i].contents as? SavedStickerItem { + let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) + let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) + entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + } + } + } + if let recentStickers = recentStickers, !recentStickers.items.isEmpty { - let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: "FREQUENTLY USED", shortName: "", hash: 0, count: 0) + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", hash: 0, count: 0) for i in 0 ..< min(20, recentStickers.items.count) { if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) @@ -172,7 +188,7 @@ final class ChatMediaInputNodeInteraction { private func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> StickerPacksCollectionPosition { switch position { case let .scroll(index): - if let index = index, index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { + if let index = index, index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue || index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { return .scroll(aroundIndex: nil) } default: @@ -262,6 +278,9 @@ final class ChatMediaInputNode: ChatInputNode { var index: Int32 = 0 if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { strongSelf.setCurrentPane(.gifs, transition: .animated(duration: 0.25, curve: .spring)) + } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { + strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) + strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil))) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil))) @@ -293,13 +312,13 @@ final class ChatMediaInputNode: ChatInputNode { |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in switch position { case .initial: - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in return (view, .generic) } case let .scroll(aroundIndex): var firstTime = true - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140) + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 140) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { @@ -312,7 +331,7 @@ final class ChatMediaInputNode: ChatInputNode { } case let .navigate(index): var firstTime = true - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 140) + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 140) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { @@ -335,15 +354,17 @@ final class ChatMediaInputNode: ChatInputNode { let (view, update) = viewAndUpdate let (theme, strings) = themeAndStrings + var savedStickers: OrderedItemListView? var recentStickers: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { recentStickers = orderedView - break + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { + savedStickers = orderedView } } - let panelEntries = chatMediaInputPanelEntries(view: view, recentStickers: recentStickers, theme: theme) - let gridEntries = chatMediaInputGridEntries(view: view, recentStickers: recentStickers, strings: strings, theme: theme) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, theme: theme) + let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) } @@ -351,8 +372,7 @@ final class ChatMediaInputNode: ChatInputNode { self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in if let strongSelf = self { strongSelf.currentView = view - strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime) - strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) + strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) } })) @@ -470,7 +490,7 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.ensureItemNodeVisible(itemNode) ensuredNodeVisible = true } - } else if let itemNode = itemNode as? ChatMediaInputRecentStickerPacksItemNode { + } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { self.listView.ensureItemNodeVisible(itemNode) @@ -609,7 +629,7 @@ final class ChatMediaInputNode: ChatInputNode { return panelHeight } - private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) { + private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool, thenGridTransition gridTransition: ChatMediaInputGridTransition, gridFirstTime: Bool) { var options = ListViewDeleteAndInsertOptions() if firstTime { options.insert(.Synchronous) @@ -618,11 +638,18 @@ final class ChatMediaInputNode: ChatInputNode { options.insert(.AnimateInsertion) } self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) + } }) } private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) { - self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) + var itemTransition: ContainedViewLayoutTransition = .immediate + if transition.animated { + itemTransition = .animated(duration: 0.3, curve: .spring) + } + self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) } @objc func previewGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { diff --git a/TelegramUI/ChatMediaInputPanelEntries.swift b/TelegramUI/ChatMediaInputPanelEntries.swift index c526558b1f..6b916edeff 100644 --- a/TelegramUI/ChatMediaInputPanelEntries.swift +++ b/TelegramUI/ChatMediaInputPanelEntries.swift @@ -5,12 +5,14 @@ import Display enum ChatMediaInputPanelAuxiliaryNamespace: Int32 { case recentGifs = 3 - case recentStickers = 2 - case trending = 4 + case savedStickers = 2 + case recentStickers = 4 + case trending = 5 } enum ChatMediaInputPanelEntryStableId: Hashable { case recentGifs + case savedStickers case recentPacks case stickerPack(Int64) @@ -22,6 +24,12 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } else { return false } + case .savedStickers: + if case .savedStickers = rhs { + return true + } else { + return false + } case .recentPacks: if case .recentPacks = rhs { return true @@ -41,8 +49,10 @@ enum ChatMediaInputPanelEntryStableId: Hashable { switch self { case .recentGifs: return 0 - case .recentPacks: + case .savedStickers: return 1 + case .recentPacks: + return 2 case let .stickerPack(id): return id.hashValue } @@ -51,6 +61,7 @@ enum ChatMediaInputPanelEntryStableId: Hashable { enum ChatMediaInputPanelEntry: Comparable, Identifiable { case recentGifs(PresentationTheme) + case savedStickers(PresentationTheme) case recentPacks(PresentationTheme) case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) @@ -58,6 +69,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch self { case .recentGifs: return .recentGifs + case .savedStickers: + return .savedStickers case .recentPacks: return .recentPacks case let .stickerPack(_, info, _, _): @@ -73,6 +86,12 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return false } + case let .savedStickers(lhsTheme): + if case let .savedStickers(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } case let .recentPacks(lhsTheme): if case let .recentPacks(rhsTheme) = rhs, lhsTheme === rhsTheme { return true @@ -97,16 +116,23 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } + case .savedStickers: + switch rhs { + case .recentGifs, savedStickers: + return false + default: + return true + } case .recentPacks: switch rhs { - case .recentGifs, recentPacks: + case .recentGifs, .savedStickers, recentPacks: return false default: return true } case let .stickerPack(lhsIndex, lhsInfo, _, _): switch rhs { - case .recentGifs, .recentPacks: + case .recentGifs, .savedStickers, .recentPacks: return false case let .stickerPack(rhsIndex, rhsInfo, _, _): if lhsIndex == rhsIndex { @@ -125,8 +151,13 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) + case let .savedStickers(theme): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, selected: { + let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) + inputNodeInteraction.navigateToCollectionId(collectionId) + }) case let .recentPacks(theme): - return ChatMediaInputRecentStickerPacksItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 3e91c1fe8b..bb67591c34 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -17,7 +17,7 @@ final class ChatMediaInputStickerPane: ASDisplayNode { } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) } diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index b3bd57d4de..38c31ceaf3 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -10,6 +10,7 @@ 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 textFixedFont: UIFont = Font.regular(15.0) +private let buttonFont: UIFont = Font.semibold(13.0) struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { var rawValue: Int32 @@ -26,12 +27,139 @@ struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) } +private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { + private let textNode: TextNode + private let highlightedTextNode: TextNode + private let backgroundNode: ASImageNode + + private var regularImage: UIImage? + private var highlightedImage: UIImage? + + var pressed: (() -> Void)? + + override init() { + self.textNode = TextNode() + self.textNode.isLayerBacked = true + self.highlightedTextNode = TextNode() + self.highlightedTextNode.isLayerBacked = true + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + self.addSubnode(self.highlightedTextNode) + self.highlightedTextNode.isHidden = true + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.image = strongSelf.highlightedImage + strongSelf.textNode.isHidden = true + strongSelf.highlightedTextNode.isHidden = false + } else { + UIView.transition(with: strongSelf.view, duration: 0.2, options: [.transitionCrossDissolve], animations: { + strongSelf.backgroundNode.image = strongSelf.regularImage + strongSelf.textNode.isHidden = false + strongSelf.highlightedTextNode.isHidden = true + }, completion: nil) + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc func buttonPressed() { + self.pressed?() + } + + static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) { + let previousRegularImage = current?.regularImage + let previousHighlightedImage = current?.highlightedImage + + let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) + let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout) + + return { width, regularImage, highlightedImage, title, titleColor, highlightedTitleColor in + let targetNode: ChatMessageAttachedContentButtonNode + if let current = current { + targetNode = current + } else { + targetNode = ChatMessageAttachedContentButtonNode() + } + + let makeTextLayout: (NSAttributedString?, UIColor?, Int, CTLineTruncationType, CGSize, NSTextAlignment, TextNodeCutout?, UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) + if let maybeMakeTextLayout = maybeMakeTextLayout { + makeTextLayout = maybeMakeTextLayout + } else { + makeTextLayout = TextNode.asyncLayout(targetNode.textNode) + } + + let makeHighlightedTextLayout: (NSAttributedString?, UIColor?, Int, CTLineTruncationType, CGSize, NSTextAlignment, TextNodeCutout?, UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) + if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout { + makeHighlightedTextLayout = maybeMakeHighlightedTextLayout + } else { + makeHighlightedTextLayout = TextNode.asyncLayout(targetNode.highlightedTextNode) + } + + var updatedRegularImage: UIImage? + if regularImage !== previousRegularImage { + updatedRegularImage = regularImage + } + + var updatedHighlightedImage: UIImage? + if highlightedImage !== previousHighlightedImage { + updatedHighlightedImage = highlightedImage + } + + let labelInset: CGFloat = 8.0 + + let (textSize, textApply) = makeTextLayout(NSAttributedString(string: title, font: buttonFont, textColor: titleColor), nil, 1, .end, CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), .left, nil, UIEdgeInsets()) + + let (_, highlightedTextApply) = makeHighlightedTextLayout(NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), nil, 1, .end, CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), .left, nil, UIEdgeInsets()) + + return (textSize.size.width + labelInset * 2.0, { refinedWidth in + return (CGSize(width: refinedWidth, height: 33.0), { + if let updatedRegularImage = updatedRegularImage { + targetNode.regularImage = updatedRegularImage + if !targetNode.textNode.isHidden { + targetNode.backgroundNode.image = updatedRegularImage + } + } + if let updatedHighlightedImage = updatedHighlightedImage { + targetNode.highlightedImage = updatedHighlightedImage + if targetNode.textNode.isHidden { + targetNode.backgroundNode.image = updatedHighlightedImage + } + } + + let _ = textApply() + let _ = highlightedTextApply() + + targetNode.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0)) + targetNode.textNode.frame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((33.0 - textSize.size.height) / 2.0)), size: textSize.size) + targetNode.highlightedTextNode.frame = targetNode.textNode.frame + + return targetNode + }) + }) + } + } +} + final class ChatMessageAttachedContentNode: ASDisplayNode { private let lineNode: ASImageNode private let textNode: TextNode private let inlineImageNode: TransformImageNode private var contentImageNode: ChatMessageInteractiveMediaNode? private var contentFileNode: ChatMessageInteractiveFileNode? + private var buttonBackgroundNode: ASImageNode? + private var buttonNode: ChatMessageAttachedContentButtonNode? private let statusNode: ChatMessageDateAndStatusNode @@ -73,7 +201,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ automaticDownloadSettings: AutomaticMediaDownloadSettings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let textAsyncLayout = TextNode.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() @@ -81,7 +209,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) - return { theme, strings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, displayLine, layoutConstants, position, constrainedSize in + let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) + + return { theme, strings, automaticDownloadSettings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, actionTitle, displayLine, layoutConstants, position, constrainedSize in let incoming = message.effectivelyIncoming var insets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 5.0, right: 8.0) @@ -172,20 +302,27 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.isVideo { - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + var automaticDownload = false + if file.isAnimated { + automaticDownload = automaticDownloadSettings.categories.getGif(message.id.peerId) + } else if file.isInstantVideo { + automaticDownload = automaticDownloadSettings.categories.getInstantVideo(message.id.peerId) + } + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, file, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { var automaticDownload = false if file.isVoice { - automaticDownload = true + automaticDownload = automaticDownloadSettings.categories.getVoice(message.id.peerId) } - let (_, refineLayout) = contentFileLayout(account, theme, strings, message, file, automaticDownload,message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(account, theme, strings, message, file, automaticDownload, message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId) + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -196,7 +333,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } } else if let image = media as? TelegramMediaWebFile { - let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId) + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), automaticDownload, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } @@ -265,10 +403,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme) var boundingSize = textFrame.size + var lineHeight = textFrame.size.height if let statusFrame = statusFrame { boundingSize = textFrame.union(statusFrame).size + if let _ = actionTitle { + lineHeight = boundingSize.height + } } - var lineHeight = textFrame.size.height if let inlineImageSize = inlineImageSize { if boundingSize.height < inlineImageSize.height { boundingSize.height = inlineImageSize.height @@ -293,8 +434,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { boundingSize.width = max(boundingSize.width, refinedWidth) } - boundingSize.width += insets.left + insets.right - boundingSize.height += insets.top + insets.bottom lineHeight += insets.top + insets.bottom var imageApply: (() -> Void)? @@ -304,6 +443,31 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { imageApply = imageLayout(arguments) } + var continueActionButtonLayout: ((CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))? + if let actionTitle = actionTitle { + let buttonImage: UIImage + let buttonHighlightedImage: UIImage + let titleColor: UIColor + let titleHighlightedColor: UIColor + if incoming { + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(theme)! + titleColor = theme.chat.bubble.incomingAccentColor + titleHighlightedColor = theme.chat.bubble.incomingFillColor + } else { + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(theme)! + titleColor = theme.chat.bubble.outgoingAccentColor + titleHighlightedColor = theme.chat.bubble.outgoingFillColor + } + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, actionTitle, titleColor, titleHighlightedColor) + boundingSize.width = max(buttonWidth, boundingSize.width) + continueActionButtonLayout = continueLayout + } + + boundingSize.width += insets.left + insets.right + boundingSize.height += insets.top + insets.bottom + return (boundingSize.width, { boundingWidth in var adjustedBoundingSize = boundingSize var adjustedLineHeight = lineHeight @@ -341,9 +505,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedLineHeight += imageHeigthAddition + 4.0 } - /*if let _ = webPageContent?.instantPage { - adjustedBoundingSize.height += 4.0 - }*/ + var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))? + if let continueActionButtonLayout = continueActionButtonLayout { + let (size, apply) = continueActionButtonLayout(boundingWidth - 9.0 - insets.right) + actionButtonSizeAndApply = (size, apply) + adjustedBoundingSize.height += 7.0 + size.height + } var adjustedStatusFrame: CGRect? if let statusFrame = statusFrame { @@ -442,6 +609,24 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + if let (size, apply) = actionButtonSizeAndApply { + let buttonNode = apply() + if buttonNode !== strongSelf.buttonNode { + strongSelf.buttonNode?.removeFromSupernode() + strongSelf.buttonNode = buttonNode + strongSelf.addSubnode(buttonNode) + buttonNode.pressed = { + if let strongSelf = self { + strongSelf.activateAction?() + } + } + } + buttonNode.frame = CGRect(origin: CGPoint(x: 9.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size) + } else if let buttonNode = strongSelf.buttonNode { + buttonNode.removeFromSupernode() + strongSelf.buttonNode = nil + } + if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame { strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) if strongSelf.statusNode.supernode == nil { diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 58b122a4c3..a98dfb2b01 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -79,6 +79,9 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateHiddenMedia(_ media: [Media]?) { } + func updateAutomaticMediaDownloadSettings(_ settings: AutomaticMediaDownloadSettings) { + } + func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { return .none } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 830c98ae75..52cec7c8d1 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -1035,6 +1035,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } + override func updateAutomaticMediaDownloadSettings() { + if let item = self.item, let controllerInteraction = self.controllerInteraction { + for contentNode in self.contentNodes { + contentNode.updateAutomaticMediaDownloadSettings(controllerInteraction.automaticMediaDownloadSettings) + } + } + } + override func updateSelectionState(animated: Bool) { guard let controllerInteraction = self.controllerInteraction else { return diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 61bbe195b0..6446136600 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -61,7 +61,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { var automaticDownload = false if selectedFile!.isVoice { - automaticDownload = true + automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getVoice(item.message.id.peerId) } let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.theme, item.strings, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index 1e94a6577a..17dd2ac176 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -71,7 +71,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, true, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, true, layoutConstants, position, constrainedSize) return (initialWidth, { constrainedSize in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index a0966544d4..14111ffb68 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -22,7 +22,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { private let consumableContentNode: ASImageNode private var iconNode: TransformImageNode? - private var progressNode: RadialProgressNode? + private var statusNode: RadialStatusNode? private var tapRecognizer: UITapGestureRecognizer? private let statusDisposable = MetaDisposable() @@ -282,6 +282,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let minVoiceWidth: CGFloat = 120.0 let maxVoiceWidth = constrainedSize.width let maxVoiceLength: CGFloat = 30.0 + let minVoiceLength: CGFloat = 2.0 let minLayoutWidth: CGFloat if isVoice { @@ -289,12 +290,15 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { //b = log (y1/y2) / (x1-x2) //a = y1 / exp bx1 - let b = log(maxVoiceWidth / minVoiceWidth) / (maxVoiceLength - 0.0) + /*let b = log(maxVoiceWidth / minVoiceWidth) / (maxVoiceLength) let a = minVoiceWidth / exp(CGFloat(0.0)) let y = a * exp(b * min(maxVoiceLength, CGFloat(audioDuration))) - minLayoutWidth = floor(y) + minLayoutWidth = floor(y)*/ + + let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration))) + minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength) } else { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 } @@ -379,47 +383,62 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let strongSelf = strongSelf { strongSelf.resourceStatus = status - if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) - strongSelf.progressNode = progressNode - progressNode.frame = progressFrame - strongSelf.addSubnode(progressNode) + if strongSelf.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor) + strongSelf.statusNode = statusNode + statusNode.frame = progressFrame + strongSelf.addSubnode(statusNode) } else if let _ = updatedTheme { - strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) + //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) } + let state: RadialStatusNodeState + let statusForegroundColor = incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor switch status { case let .fetchStatus(fetchStatus): switch fetchStatus { case let .Fetching(progress): - strongSelf.progressNode?.state = .Fetching(progress: progress) + state = .progress(color: statusForegroundColor, value: CGFloat(progress), cancelEnabled: true) case .Local: if isAudio { - strongSelf.progressNode?.state = .Play + state = .play(statusForegroundColor) + } else if let fileIconImage = fileIconImage { + state = .customIcon(fileIconImage) } else { - strongSelf.progressNode?.state = .Icon + state = .none } case .Remote: if isAudio { - strongSelf.progressNode?.state = .Play + state = .play(statusForegroundColor) } else { - strongSelf.progressNode?.state = .Remote + state = .download(statusForegroundColor) } } case let .playbackStatus(playbackStatus): switch playbackStatus { case .playing: - strongSelf.progressNode?.state = .Pause + state = .pause(statusForegroundColor) case .paused: - strongSelf.progressNode?.state = .Play + state = .play(statusForegroundColor) } } + + if let statusNode = strongSelf.statusNode { + if state == .none { + strongSelf.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } } } })) } - strongSelf.progressNode?.frame = progressFrame + strongSelf.statusNode?.frame = progressFrame if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 6456414718..a76cd1d963 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -13,7 +13,7 @@ private struct FetchControls { final class ChatMessageInteractiveMediaNode: ASTransformNode { private let imageNode: TransformImageNode private var videoNode: ManagedVideoNode? - private var progressNode: RadialProgressNode? + private var statusNode: RadialStatusNode? private var timeoutNode: RadialTimeoutNode? private var labelNode: ChatMessageInteractiveMediaLabelNode? private var tapRecognizer: UITapGestureRecognizer? @@ -93,15 +93,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - /*if let file = self.media as? TelegramMediaFile, let message = self.message, (file.isVideo || file.isAnimated || file.mimeType.hasPrefix("video/")) && !message.containsSecretMedia { + if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { self.activateLocalContent() - } else {*/ - if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { - self.activateLocalContent() - } else { - self.progressPressed() - } - //} + } else { + self.progressPressed() + } } } @@ -173,7 +169,15 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } return (maxWidth, updatedCorners, { constrainedSize in - return (min(maxWidth, nativeSize.width), { boundingWidth in + let resultWidth: CGFloat + + if isSecretMedia { + resultWidth = maxWidth + } else { + resultWidth = min(maxWidth, nativeSize.width) + } + + return (resultWidth, { boundingWidth in let drawingSize: CGSize let boundingSize: CGSize @@ -301,7 +305,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.media = media strongSelf.themeAndStrings = (theme, strings) strongSelf.imageNode.frame = imageFrame - strongSelf.progressNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) + strongSelf.statusNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) strongSelf.timeoutNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) if replaceVideoNode { @@ -346,9 +350,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.timeoutNode?.updateTheme(backgroundColor: updatedTheme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: updatedTheme.chat.bubble.mediaOverlayControlForegroundColor) } - if let progressNode = strongSelf.progressNode { - progressNode.removeFromSupernode() - strongSelf.progressNode = nil + if let statusNode = strongSelf.statusNode { + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + strongSelf.statusNode = nil } } else if let timeoutNode = strongSelf.timeoutNode { timeoutNode.removeFromSupernode() @@ -375,43 +381,54 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } if progressRequired { - if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) - progressNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) - progressNode.position = strongSelf.imageNode.position - strongSelf.progressNode = progressNode - strongSelf.addSubnode(progressNode) + if strongSelf.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) + statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) + statusNode.position = strongSelf.imageNode.position + strongSelf.statusNode = statusNode + strongSelf.addSubnode(statusNode) } else if let _ = updatedTheme { - strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) + + //strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) } } else { - if let progressNode = strongSelf.progressNode { - progressNode.removeFromSupernode() - strongSelf.progressNode = nil + if let statusNode = strongSelf.statusNode { + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + strongSelf.statusNode = nil } } - var state: RadialProgressState - var hide = false + var state: RadialStatusNodeState + let bubbleTheme = theme.chat.bubble switch status { case let .Fetching(progress): - state = .Fetching(progress: progress) + state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(progress), cancelEnabled: true) case .Local: - state = .None + state = .none if isSecretMedia && secretProgressIcon != nil { - state = .Image(secretProgressIcon!) + state = .customIcon(secretProgressIcon!) } else if let file = media as? TelegramMediaFile { if !isInlinePlayableVideo && file.isVideo { - state = .Play + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } else { - hide = true + state = .none } } case .Remote: - state = .Remote + state = .download(bubbleTheme.mediaOverlayControlForegroundColor) + } + if let statusNode = strongSelf.statusNode { + if state == .none { + strongSelf.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) } - strongSelf.progressNode?.state = state - strongSelf.progressNode?.isHidden = hide } } })) diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index 19d4340fca..54b90ce815 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -59,7 +59,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, false, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, false, layoutConstants, position, constrainedSize) return (initialWidth, { constrainedSize in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index d6a276002c..e6dc20c6da 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -13,12 +13,15 @@ private func mediaIsNotMergeable(_ media: Media) -> Bool { if let _ = media as? TelegramMediaAction { return true } + if let _ = media as? TelegramMediaExpiredContent { + return true + } return false } private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool { - if abs(lhs.timestamp - rhs.timestamp) < 5 * 60 && lhs.author?.id == rhs.author?.id { + if abs(lhs.timestamp - rhs.timestamp) < Int32(5 * 60) && lhs.author?.id == rhs.author?.id { for media in lhs.media { if mediaIsNotMergeable(media) { return false diff --git a/TelegramUI/ChatMessageItemContent.swift b/TelegramUI/ChatMessageItemContent.swift new file mode 100644 index 0000000000..0ac184db5b --- /dev/null +++ b/TelegramUI/ChatMessageItemContent.swift @@ -0,0 +1,16 @@ +import Foundation +import Display + +class ChatMessageItemContent { + func attach(node: ASDisplayNode) { + preconditionFailure() + } + + func detach() { + preconditionFailure() + } + + func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + preconditionFailure() + } +} diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 865b9b4012..77bdaa6179 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -64,6 +64,8 @@ public class ChatMessageItemView: ListViewItemNode { var item: ChatMessageItem? var controllerInteraction: ChatControllerInteraction? + private var content: ChatMessageItemContent? + public required convenience init() { self.init(layerBacked: true) } @@ -112,7 +114,6 @@ public class ChatMessageItemView: ListViewItemNode { self.transitionOffset = -self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) } - //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { @@ -136,6 +137,9 @@ public class ChatMessageItemView: ListViewItemNode { func updateHighlightedState(animated: Bool) { } + func updateAutomaticMediaDownloadSettings() { + } + override public func header() -> ListViewItemHeader? { if let item = self.item { return item.header diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 6e91fc43e9..55cd6e0528 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -49,17 +49,22 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return { item, layoutConstants, position, constrainedSize in var selectedMedia: Media? + var automaticDownload: Bool = false for media in item.message.media { if let telegramImage = media as? TelegramMediaImage { selectedMedia = telegramImage + automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getPhoto(item.message.id.peerId) } else if let telegramFile = media as? TelegramMediaFile { selectedMedia = telegramFile + if telegramFile.isAnimated { + automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getGif(item.message.id.peerId) + } } } let initialImageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item.theme, item.strings, item.message, selectedMedia!, initialImageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhotos, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) + let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item.theme, item.strings, item.message, selectedMedia!, initialImageCorners, automaticDownload, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index a1242eb7c7..7be0ce47d6 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -47,12 +47,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true self.titleNode.maximumNumberOfLines = 1 - //self.titleNode.contentMode = .topLeft self.textNode = ASTextNode() self.textNode.isLayerBacked = true self.textNode.maximumNumberOfLines = 2 - //self.textNode.contentMode = .topLeft self.imageNode = TransformImageNode() @@ -144,7 +142,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } selectedText = true break loop - case let .Sticker(displayText, _): + case let .Sticker(displayText, _, _): messageText = "\(displayText) Sticker" selectedText = true break loop diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index c7151feae4..7c7e17739e 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -26,7 +26,7 @@ func textStringForReplyMessage(_ message: Message) -> (String, Bool) { var fileName: String = "File" for attribute in file.attributes { switch attribute { - case let .Sticker(text, _): + case let .Sticker(text, _, _): return ("\(text) Sticker", true) case let .FileName(name): fileName = name diff --git a/TelegramUI/ChatMessageThrottledProcessingManager.swift b/TelegramUI/ChatMessageThrottledProcessingManager.swift index 713c3a865c..3f407488c4 100644 --- a/TelegramUI/ChatMessageThrottledProcessingManager.swift +++ b/TelegramUI/ChatMessageThrottledProcessingManager.swift @@ -5,12 +5,19 @@ import SwiftSignalKit final class ChatMessageThrottledProcessingManager { private let queue = Queue(target: Queue.concurrentBackgroundQueue()) + private let delay: Double + var process: ((Set) -> Void)? private var timer: SwiftSignalKit.Timer? + private var processedList: [MessageId] = [] private var processed = Set() private var buffer = Set() + init(delay: Double = 1.0) { + self.delay = delay + } + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process @@ -22,13 +29,21 @@ final class ChatMessageThrottledProcessingManager { for id in messageIds { if !self.processed.contains(id) { self.processed.insert(id) + self.processedList.append(id) self.buffer.insert(id) } } + if self.processedList.count > 1000 { + for i in 0 ..< 200 { + self.processed.remove(self.processedList[i]) + } + self.processedList.removeSubrange(0 ..< 200) + } + if self.timer == nil { var completionImpl: (() -> Void)? - let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { + let timer = SwiftSignalKit.Timer(timeout: self.delay, repeat: false, completion: { completionImpl?() }, queue: self.queue) completionImpl = { [weak self, weak timer] in diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 6e88241d93..59797355a3 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -32,6 +32,22 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { controllerInteraction.openMessage(item.message.id) } } + self.contentNode.activateAction = { [weak self] in + if let strongSelf = self, let item = strongSelf.item, let controllerInteraction = strongSelf.controllerInteraction { + var webPageContent: TelegramMediaWebpageLoadedContent? + for media in item.message.media { + if let media = media as? TelegramMediaWebpage { + if case let .Loaded(content) = media.content { + webPageContent = content + } + break + } + } + if let webpage = webPageContent { + controllerInteraction.openUrl(webpage.url) + } + } + } } required init?(coder aDecoder: NSCoder) { @@ -58,6 +74,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { var subtitle: String? var text: String? var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? + var actionTitle: String? if let webpage = webPageContent { if let websiteName = webpage.websiteName, !websiteName.isEmpty { @@ -89,9 +106,22 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (image, [.preferMediaInline]) } } + + if let _ = webpage.instantPage { + actionTitle = item.strings.Conversation_InstantPagePreview + } else if let type = webpage.type { + switch type { + case "telegram_channel": + actionTitle = item.strings.Conversation_ViewChannel + case "telegram_supergroup": + actionTitle = item.strings.Conversation_ViewGroup + default: + break + } + } } - let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, true, layoutConstants, position, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, actionTitle, true, layoutConstants, position, constrainedSize) return (initialWidth, { constrainedSize in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) @@ -137,6 +167,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return .instantPage } } + return .ignore } return .none } diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 4720356b93..b31fb7de38 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -55,9 +55,11 @@ final class ChatPanelInterfaceInteraction { let reportPeer: () -> Void let dismissReportPeer: () -> Void let deleteChat: () -> Void + let beginCall: () -> Void + let toggleMessageStickerStarred: (MessageId) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -88,6 +90,8 @@ final class ChatPanelInterfaceInteraction { self.reportPeer = reportPeer self.dismissReportPeer = dismissReportPeer self.deleteChat = deleteChat + self.beginCall = beginCall + self.toggleMessageStickerStarred = toggleMessageStickerStarred self.statuses = statuses } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 3fa98cef9d..d11701c648 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -46,7 +46,7 @@ enum ChatPresentationInputQuery: Equatable { } enum ChatPresentationInputQueryResult: Equatable { - case stickers([StickerPackItem]) + case stickers([FoundStickerItem]) case hashtags([String]) case mentions([Peer]) case commands([PeerCommand]) @@ -219,6 +219,7 @@ struct ChatPresentationInterfaceState: Equatable { let keyboardButtonsMessage: Message? let pinnedMessageId: MessageId? let peerIsBlocked: Bool + let peerIsMuted: Bool let canReportPeer: Bool let chatHistoryState: ChatHistoryNodeHistoryState? let botStartPayload: String? @@ -238,6 +239,7 @@ struct ChatPresentationInterfaceState: Equatable { self.keyboardButtonsMessage = nil self.pinnedMessageId = nil self.peerIsBlocked = false + self.peerIsMuted = false self.canReportPeer = false self.chatHistoryState = nil self.botStartPayload = nil @@ -248,7 +250,7 @@ struct ChatPresentationInterfaceState: Equatable { self.strings = strings } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState @@ -258,6 +260,7 @@ struct ChatPresentationInterfaceState: Equatable { self.keyboardButtonsMessage = keyboardButtonsMessage self.pinnedMessageId = pinnedMessageId self.peerIsBlocked = peerIsBlocked + self.peerIsMuted = peerIsMuted self.canReportPeer = canReportPeer self.chatHistoryState = chatHistoryState self.botStartPayload = botStartPayload @@ -319,6 +322,10 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.peerIsMuted != rhs.peerIsMuted { + return false + } + if lhs.chatHistoryState != rhs.chatHistoryState { return false } @@ -358,58 +365,62 @@ struct ChatPresentationInterfaceState: Equatable { } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + } + + func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } } diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 5c029e3f0c..90a690ef5f 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -47,7 +47,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.resultsLabel = TextNode() self.resultsLabel.isLayerBacked = true self.resultsLabel.displaysAsynchronously = false - self.activityIndicator = ActivityIndicator(theme: theme) + self.activityIndicator = ActivityIndicator(type: .navigationAccent(theme)) self.activityIndicator.isHidden = true super.init() diff --git a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift index d8f1bdcb14..a964217ef4 100644 --- a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift +++ b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift @@ -80,7 +80,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp, theme: self.theme) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index b6e211b441..2a84bf8b56 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -4,7 +4,7 @@ import Display import Postbox import TelegramCore import SwiftSignalKit -import TelegramLegacyComponents +import LegacyComponents final class ChatTitleView: UIView { private var theme: PresentationTheme @@ -59,7 +59,7 @@ final class ChatTitleView: UIView { } if self.typingIndicator == nil { let typingIndicator = TGModernConversationTitleActivityIndicator() - typingIndicator.setColor(self.theme.rootController.navigationBar.accentTextColor) + //typingIndicator.setColor(self.theme.rootController.navigationBar.accentTextColor) self.addSubview(typingIndicator) self.typingIndicator = typingIndicator } diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index e9f18b3ded..787d887199 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -46,9 +46,11 @@ final class ComposeControllerNode: ASDisplayNode { }) ])) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor diff --git a/TelegramUI/ContactMultiselectionControllerNode.swift b/TelegramUI/ContactMultiselectionControllerNode.swift index 237a942914..885e7626fd 100644 --- a/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/TelegramUI/ContactMultiselectionControllerNode.swift @@ -52,9 +52,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: false, options: []), selectionState: ContactListNodeGroupSelectionState()) self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.chatList.searchBarKeyboardColor), placeholder: self.presentationData.strings.Compose_TokenListPlaceholder) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index 5d1f5f7be0..d1704fa1e2 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -29,9 +29,11 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index fa7f396242..0720d68d7d 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -27,9 +27,11 @@ final class ContactsControllerNode: ASDisplayNode { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index ed78e67363..501968dabd 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private enum AutomaticDownloadCategory { case photo diff --git a/TelegramUI/EmbedVideoNode.swift b/TelegramUI/EmbedVideoNode.swift index 04818c2a69..49a3f01e8f 100644 --- a/TelegramUI/EmbedVideoNode.swift +++ b/TelegramUI/EmbedVideoNode.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdge, view: TGEmbedPIPPullArrowView) { let arrowX: CGFloat @@ -34,9 +34,17 @@ private final class SharedEmbedVideoContext: SharedVideoContext { var ready: Signal { return self._ready.get() } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + private let thumbnail = Promise() private var thumbnailDisposable: Disposable? + private var loadProgressDisposable: Disposable? + init(account: Account, audioSessionManager: ManagedAudioSession, webpage: TelegramMediaWebpageLoadedContent) { let converted = TGWebPageMediaAttachment() @@ -79,6 +87,21 @@ private final class SharedEmbedVideoContext: SharedVideoContext { super.init() + let nativeLoadProgress = self.playerView.loadProgress() + let loadProgress: Signal = Signal { subscriber in + let disposable = nativeLoadProgress?.start(next: { value in + subscriber.putNext((value as! NSNumber).floatValue) + }) + return ActionDisposable { + disposable?.dispose() + } + } + self.loadProgressDisposable = (loadProgress |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf._preloadCompleted.set(value.isEqual(to: 1.0)) + } + }) + if let image = webpage.image { self.thumbnailDisposable = (rawMessagePhoto(account: account, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in if let strongSelf = self { @@ -90,7 +113,7 @@ private final class SharedEmbedVideoContext: SharedVideoContext { self._ready.set(.single()) } - self.playerView.requestAudioSession = { [weak self] in + /*self.playerView.requestAudioSession = { [weak self] in assert(Queue.mainQueue().isCurrent()) if let strongSelf = self, !strongSelf.hasAudioSession { strongSelf.audioSessionDisposable.set(audioSessionManager.push(audioSessionType: .play, overrideSpeaker: false, once: false, activate: { @@ -120,13 +143,16 @@ private final class SharedEmbedVideoContext: SharedVideoContext { strongSelf.hasAudioSession = false strongSelf.audioSessionDisposable.set(nil) } - } + }*/ self.playerView.stateSignal() } deinit { - audioSessionDisposable.dispose() + self.audioSessionDisposable.dispose() + + self.loadProgressDisposable?.dispose() + self.thumbnailDisposable?.dispose() } func play() { @@ -205,13 +231,13 @@ final class EmbedVideoNode: OverlayMediaItemNode { private let backgroundNode: ASImageNode private let imageNode: TransformImageNode private var snapshotView: UIView? - private let progressNode: RadialProgressNode + private var statusNode: RadialStatusNode? private let controlsNode: PictureInPictureVideoControlsNode? private var minimizedBlurView: UIVisualEffectView? private var minimizedArrowView: TGEmbedPIPPullArrowView? private var minimizedEdge: OverlayMediaItemMinimizationEdge? - private var statusDisposable: Disposable? + private var preloadDisposable: Disposable? var tapped: (() -> Void)? var dismissed: (() -> Void)? @@ -228,7 +254,7 @@ final class EmbedVideoNode: OverlayMediaItemNode { } override var group: OverlayMediaItemNodeGroup? { - return OverlayMediaItemNodeGroup(rawValue: 0) + return OverlayMediaItemNodeGroup(rawValue: 1) } override var isMinimizeable: Bool { @@ -249,7 +275,6 @@ final class EmbedVideoNode: OverlayMediaItemNode { self.backgroundNode.displaysAsynchronously = false self.imageNode = TransformImageNode() - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor(white: 1.0, alpha: 1.0), icon: nil)) var leaveImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? @@ -294,6 +319,9 @@ final class EmbedVideoNode: OverlayMediaItemNode { if withOverlayControls { self.backgroundNode.image = backgroundImage + + self.layer.masksToBounds = true + self.layer.cornerRadius = 2.5 } self.addSubnode(self.backgroundNode) @@ -324,6 +352,8 @@ final class EmbedVideoNode: OverlayMediaItemNode { manager.sharedVideoContextManager.detachSharedVideoContext(id: source.id, index: contextId) } } + + self.preloadDisposable?.dispose() } override func didLoad() { @@ -388,13 +418,17 @@ final class EmbedVideoNode: OverlayMediaItemNode { context.playerView.transform = CGAffineTransform(scaleX: videoFrame.size.width / context.intrinsicSize.width, y: videoFrame.size.height / context.intrinsicSize.height) } - let backgroundInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) + let backgroundInsets = UIEdgeInsets(top: 11.5, left: 13.5, bottom: 11.5, right: 13.5) self.backgroundNode.frame = CGRect(origin: CGPoint(x: -backgroundInsets.left, y: -backgroundInsets.top), size: CGSize(width: videoFrame.size.width + backgroundInsets.left + backgroundInsets.right, height: videoFrame.size.height + backgroundInsets.top + backgroundInsets.bottom)) self.imageNode.asyncLayout()(arguments)() self.imageNode.frame = videoFrame self.snapshotView?.frame = self.imageNode.frame + if let statusNode = self.statusNode { + statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) + } + if let controlsNode = self.controlsNode { controlsNode.frame = videoFrame controlsNode.updateLayout(size: videoFrame.size, transition: .immediate) @@ -499,6 +533,28 @@ final class EmbedVideoNode: OverlayMediaItemNode { } }) self._ready.set(context.ready) + + self.preloadDisposable = (context.preloadCompleted |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if value { + if let statusNode = strongSelf.statusNode { + strongSelf.statusNode = nil + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + } + } else { + if strongSelf.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) + strongSelf.statusNode = statusNode + strongSelf.addSubnode(statusNode) + let size = strongSelf.bounds.size + statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) + statusNode.transitionToState(.progress(color: .white, value: nil, cancelEnabled: false), completion: {}) + } + } + } + }) } }) } @@ -575,10 +631,10 @@ final class EmbedVideoNode: OverlayMediaItemNode { self.minimizedBlurView?.isHidden = false switch edge { - case .left: - break - case .right: - break + case .left: + break + case .right: + break } } diff --git a/TelegramUI/FFMpegAudioFrameDecoder.swift b/TelegramUI/FFMpegAudioFrameDecoder.swift index 0915ac9bdb..f839b537f7 100644 --- a/TelegramUI/FFMpegAudioFrameDecoder.swift +++ b/TelegramUI/FFMpegAudioFrameDecoder.swift @@ -24,7 +24,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { } func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { - var status = avcodec_send_packet(self.codecContext, frame.packet) + var status = frame.packet.sendToDecoder(self.codecContext) if status == 0 { status = avcodec_receive_frame(self.codecContext, self.audioFrame) if status == 0 { diff --git a/TelegramUI/FFMpegMediaFrameSource.swift b/TelegramUI/FFMpegMediaFrameSource.swift index aa6a74e76d..505c75d4ad 100644 --- a/TelegramUI/FFMpegMediaFrameSource.swift +++ b/TelegramUI/FFMpegMediaFrameSource.swift @@ -199,8 +199,8 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.performWithContext { [weak self] context in context.initializeState(postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding) - context.seek(timestamp: timestamp, completed: { [weak self] streamDescriptions, timestamp in - queue.async { [weak self] in + context.seek(timestamp: timestamp, completed: { streamDescriptions, timestamp in + queue.async { if let strongSelf = self { var audioBuffer: MediaTrackFrameBuffer? var videoBuffer: MediaTrackFrameBuffer? diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index dbd853f582..2dc6303c37 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -26,12 +26,30 @@ struct FFMpegMediaFrameSourceDescriptionSet { let video: FFMpegMediaFrameSourceDescription? } -private struct InitializedState { +private final class InitializedState { fileprivate let avIoContext: UnsafeMutablePointer fileprivate let avFormatContext: UnsafeMutablePointer fileprivate let audioStream: StreamContext? fileprivate let videoStream: StreamContext? + + init(avIoContext: UnsafeMutablePointer, avFormatContext: UnsafeMutablePointer, audioStream: StreamContext?, videoStream: StreamContext?) { + self.avIoContext = avIoContext + self.avFormatContext = avFormatContext + self.audioStream = audioStream + self.videoStream = videoStream + } + + deinit { + let avIoContext = Optional(self.avIoContext) + if self.avIoContext.pointee.buffer != nil { + av_free(self.avIoContext.pointee.buffer) + } + av_free(avIoContext) + + var avFormatContext = Optional(self.avFormatContext) + avformat_close_input(&avFormatContext) + } } struct FFMpegMediaFrameSourceStreamContextInfo { @@ -304,8 +322,6 @@ final class FFMpegMediaFrameSourceContext: NSObject { } } - - self.initializedState = InitializedState(avIoContext: avIoContext, avFormatContext: avFormatContext, audioStream: audioStream, videoStream: videoStream) } @@ -372,7 +388,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = videoStream.fps } - let frame = MediaTrackDecodableFrame(type: .video, packet: &packet.packet, pts: pts, dts: dts, duration: duration) + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) frames.append(frame) if videoTimestamp == nil || videoTimestamp! < CMTimeGetSeconds(pts) { @@ -395,7 +411,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = audioStream.fps } - let frame = MediaTrackDecodableFrame(type: .audio, packet: &packet.packet, pts: pts, dts: dts, duration: duration) + let frame = MediaTrackDecodableFrame(type: .audio, packet: packet, pts: pts, dts: dts, duration: duration) frames.append(frame) if audioTimestamp == nil || audioTimestamp! < CMTimeGetSeconds(pts) { diff --git a/TelegramUI/FFMpegMediaPassthroughVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaPassthroughVideoFrameDecoder.swift index 8235b50135..2ae5f11ede 100644 --- a/TelegramUI/FFMpegMediaPassthroughVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaPassthroughVideoFrameDecoder.swift @@ -13,16 +13,16 @@ final class FFMpegMediaPassthroughVideoFrameDecoder: MediaTrackFrameDecoder { func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { var blockBuffer: CMBlockBuffer? - let bytes = malloc(Int(frame.packet.pointee.size))! - memcpy(bytes, frame.packet.pointee.data, Int(frame.packet.pointee.size)) - guard CMBlockBufferCreateWithMemoryBlock(nil, bytes, Int(frame.packet.pointee.size), nil, nil, 0, Int(frame.packet.pointee.size), 0, &blockBuffer) == noErr else { + let bytes = malloc(Int(frame.packet.packet.size))! + memcpy(bytes, frame.packet.packet.data, Int(frame.packet.packet.size)) + guard CMBlockBufferCreateWithMemoryBlock(nil, bytes, Int(frame.packet.packet.size), nil, nil, 0, Int(frame.packet.packet.size), 0, &blockBuffer) == noErr else { free(bytes) return nil } var timingInfo = CMSampleTimingInfo(duration: frame.duration, presentationTimeStamp: frame.pts, decodeTimeStamp: frame.dts) var sampleBuffer: CMSampleBuffer? - var sampleSize = Int(frame.packet.pointee.size) + var sampleSize = Int(frame.packet.packet.size) guard CMSampleBufferCreate(nil, blockBuffer, true, nil, nil, self.videoFormat, 1, 1, &timingInfo, 1, &sampleSize, &sampleBuffer) == noErr else { return nil } diff --git a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift index 8da725a6b5..9d3adebd04 100644 --- a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift @@ -56,7 +56,7 @@ final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? { - var status = avcodec_send_packet(self.codecContext, frame.packet) + var status = frame.packet.sendToDecoder(self.codecContext) if status == 0 { status = avcodec_receive_frame(self.codecContext, self.videoFrame) if status == 0 { diff --git a/TelegramUI/FFMpegPacket.swift b/TelegramUI/FFMpegPacket.swift index a632d12a93..56e4c40baa 100644 --- a/TelegramUI/FFMpegPacket.swift +++ b/TelegramUI/FFMpegPacket.swift @@ -4,15 +4,23 @@ import TelegramUIPrivateModule final class FFMpegPacket { var packet = AVPacket() + init() { + av_init_packet(&self.packet) + } + deinit { av_packet_unref(&self.packet) } var pts: Int64 { let avNoPtsRawValue: UInt64 = 0x8000000000000000 - let avNoPtsValue = unsafeBitCast(avNoPtsRawValue, to: Int64.self) + let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) let packetPts = self.packet.pts == avNoPtsValue ? self.packet.dts : self.packet.pts return packetPts } + + func sendToDecoder(_ codecContext: UnsafeMutablePointer) -> Int32 { + return avcodec_send_packet(codecContext, &self.packet) + } } diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index b19a02a1e7..a85c4df470 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -1,7 +1,7 @@ import Foundation import Postbox import SwiftSignalKit -import TelegramLegacyComponents +import LegacyComponents private final class VideoConversionWatcher: TGMediaVideoFileWatcher { private let update: (String, Int) -> Void diff --git a/TelegramUI/FrameworkBundle.swift b/TelegramUI/FrameworkBundle.swift index d4261b2d33..adc1f674ee 100644 --- a/TelegramUI/FrameworkBundle.swift +++ b/TelegramUI/FrameworkBundle.swift @@ -11,4 +11,3 @@ extension UIImage { self.init(named: bundleImageName, in: frameworkBundle, compatibleWith: nil) } } - diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index fa704d6ed2..3636b3103e 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -10,18 +10,18 @@ private func tagsForMessage(_ message: Message) -> MessageTags? { for media in message.media { switch media { case _ as TelegramMediaImage: - return .PhotoOrVideo + return .photoOrVideo case let file as TelegramMediaFile: if file.isVideo { if !file.isAnimated { - return .PhotoOrVideo + return .photoOrVideo } } else if file.isVoice { - return .VoiceOrInstantVideo + return .voiceOrInstantVideo } else if file.isSticker { return nil } else { - return .File + return .file } default: break diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index 962695e0f1..2340535ccf 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -38,9 +38,11 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.pager = GalleryPagerNode(pageGap: pageGap) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.pager.toggleControlsVisibility = { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/GalleryItemNode.swift b/TelegramUI/GalleryItemNode.swift index 92e7f590d9..b32f4d08f4 100644 --- a/TelegramUI/GalleryItemNode.swift +++ b/TelegramUI/GalleryItemNode.swift @@ -25,9 +25,11 @@ open class GalleryItemNode: ASDisplayNode { var baseNavigationController: () -> NavigationController? = { return nil } override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) } open func ready() -> Signal { diff --git a/TelegramUI/GameController.swift b/TelegramUI/GameController.swift index 67d5f4e208..cd2cb7a7c9 100644 --- a/TelegramUI/GameController.swift +++ b/TelegramUI/GameController.swift @@ -94,4 +94,12 @@ final class GameController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } + + override var presentationController: UIPresentationController? { + get { + return nil + } set(value) { + + } + } } diff --git a/TelegramUI/GameControllerNode.swift b/TelegramUI/GameControllerNode.swift index eb1691300b..c02d9bd03f 100644 --- a/TelegramUI/GameControllerNode.swift +++ b/TelegramUI/GameControllerNode.swift @@ -3,27 +3,66 @@ import Display import AsyncDisplayKit import WebKit +private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + final class GameControllerNode: ViewControllerTracingNode { - private let webView: WKWebView + private var webView: WKWebView? var presentationData: PresentationData init(presentationData: PresentationData, url: String) { self.presentationData = presentationData - self.webView = WKWebView() - super.init() - self.view.addSubview(self.webView) + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + + userController.add(WeakGameScriptMessageHandler { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) + } + }, name: "performAction") + + configuration.userContentController = userController + let webView = WKWebView(frame: CGRect(), configuration: configuration) + if #available(iOSApplicationExtension 9.0, *) { + webView.allowsLinkPreview = false + } + self.webView = webView + + self.view.addSubview(webView) if let parsedUrl = URL(string: url) { - self.webView.load(URLRequest(url: parsedUrl)) + webView.load(URLRequest(url: parsedUrl)) } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight))) + if let webView = self.webView { + webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight))) + } } func animateIn() { @@ -35,4 +74,18 @@ final class GameControllerNode: ViewControllerTracingNode { completion?() }) } + + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["eventName"] as? String else { + return + } + + if eventName == "share_game" || eventName == "share_score" { + + } + } } diff --git a/TelegramUI/GameControllerTitleView.swift b/TelegramUI/GameControllerTitleView.swift index 7fcf3f3fbe..6396bd53f6 100644 --- a/TelegramUI/GameControllerTitleView.swift +++ b/TelegramUI/GameControllerTitleView.swift @@ -4,7 +4,7 @@ import Display import Postbox import TelegramCore import SwiftSignalKit -import TelegramLegacyComponents +import LegacyComponents final class GameControllerTitleView: UIView { private var theme: PresentationTheme diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 310224f7ae..a1e246da24 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private final class GroupInfoArguments { let account: Account @@ -933,17 +933,15 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl })) }) }, changeProfilePhoto: { - let emptyController = LegacyEmptyController() + /*let emptyController = LegacyEmptyController() let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) + let legacyController = LegacyController(presentation: .custom, legacyController: navigationController) presentControllerImpl?(legacyController, nil) - - let mixin = TGMediaAvatarMenuMixin(parentController: emptyController, hasDeleteButton: false, personalPhoto: true)! - mixin.applicationInterface = legacyController.applicationInterface + let mixin = TGMediaAvatarMenuMixin(context: LegacyControllerContext(controller: nil), parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! let _ = currentAvatarMixin.swap(mixin) mixin.didDismiss = { [weak legacyController] in legacyController?.dismiss() @@ -974,7 +972,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } - mixin.present() + mixin.present()*/ }, pushController: { controller in pushControllerImpl?(controller) }, presentController: { controller, presentationArguments in diff --git a/TelegramUI/HashtagSearchControllerNode.swift b/TelegramUI/HashtagSearchControllerNode.swift index c0e545c8df..1b964bcfd6 100644 --- a/TelegramUI/HashtagSearchControllerNode.swift +++ b/TelegramUI/HashtagSearchControllerNode.swift @@ -19,9 +19,11 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.account = account self.listNode = ListView() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = .white diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift index 1ffd47494c..c02064a34e 100644 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -73,7 +73,7 @@ private func preparedGridEntryTransition(account: Account, from fromEntries: [St let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } return StickerEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) } @@ -151,7 +151,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { private func dequeueTransitions() { while !self.queuedTransitions.isEmpty { let transition = self.queuedTransitions.removeFirst() - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) } } @@ -183,7 +183,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.gridNode.bounds = CGRect(x: gridBounds.minX, y: gridBounds.minY, width: gridFrame.size.height, height: gridFrame.size.width) self.gridNode.position = CGPoint(x: gridFrame.size.width / 2.0, y: gridFrame.size.height / 2.0) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) let dequeue = self.validLayout == nil self.validLayout = (size, interfaceState) diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index 0e0b9b0015..e8eb0f1b33 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -6,15 +6,21 @@ final class InstantPageController: ViewController { private let account: Account private let webPage: TelegramMediaWebpage + private var presentationData: PresentationData + var controllerNode: InstantPageControllerNode { return self.displayNode as! InstantPageControllerNode } init(account: Account, webPage: TelegramMediaWebpage) { self.account = account + self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 }) + self.webPage = webPage super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .White } required init(coder aDecoder: NSCoder) { @@ -22,9 +28,7 @@ final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(account: self.account) - - self.statusBar.alpha = 0.0 + self.displayNode = InstantPageControllerNode(account: self.account, strings: self.presentationData.strings, statusBar: self.statusBar) self.displayNodeDidLoad() diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index f53aa701e3..333eca95d0 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -8,8 +8,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var webPage: TelegramMediaWebpage? + private var containerLayout: ContainerViewLayout? + private let statusBar: StatusBar + private let navigationBar: InstantPageNavigationBar private let scrollNode: ASScrollNode + private let scrollNodeHeader: ASDisplayNode var currentLayout: InstantPageLayout? var currentLayoutTiles: [InstantPageTile] = [] @@ -24,17 +28,25 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { var previousContentOffset: CGPoint? var isDeceleratingBecauseOfDragging = false - init(account: Account) { + init(account: Account, strings: PresentationStrings, statusBar: StatusBar) { self.account = account + self.statusBar = statusBar + self.navigationBar = InstantPageNavigationBar(strings: strings) self.scrollNode = ASScrollNode() + self.scrollNodeHeader = ASDisplayNode() + self.scrollNodeHeader.backgroundColor = .black - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = .white self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.scrollNodeHeader) + self.addSubnode(self.navigationBar) self.scrollNode.view.delegate = self } @@ -55,13 +67,16 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = layout - if self.scrollNode.bounds.size != layout.size { + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + let scrollInsetTop = 44.0 + statusBarHeight + + if self.scrollNode.bounds.size != layout.size || !self.scrollNode.view.contentInset.top.isEqual(to: scrollInsetTop) { if !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) { self.updateLayout() } self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - //_scrollViewHeader.frame = CGRectMake(0.0f, -2000.0f, bounds.size.width, 2000.0f); - //self.scrollView.contentInset = UIEdgeInsetsMake(_statusBarHeight + 44.0f, 0.0f, 0.0f, 0.0f); + self.scrollNodeHeader.frame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0), size: CGSize(width: layout.size.width, height: 2000.0)) + self.scrollNode.view.contentInset = UIEdgeInsetsMake(scrollInsetTop, 0.0, 0.0, 0.0) if self.visibleItemsWithViews.isEmpty && self.visibleTiles.isEmpty { self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 0.0) } @@ -282,6 +297,76 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } func updateNavigationBar(forceState: Bool = false) { + let bounds = self.scrollNode.view.bounds + let contentOffset = self.scrollNode.view.contentOffset + let delta: CGFloat + if let previousContentOffset = self.previousContentOffset { + delta = contentOffset.y - previousContentOffset.y + } else { + delta = 0.0 + } + self.previousContentOffset = contentOffset + + /*void (^block)(CGRect) = ^(CGRect navigationBarFrame) { + _navigationBar.frame = navigationBarFrame; + CGFloat navigationBarHeight = _navigationBar.bounds.size.height; + if (navigationBarHeight < FLT_EPSILON) + navigationBarHeight = 64.0f; + + CGFloat statusBarOffset = -MAX(0.0f, MIN(_statusBarHeight, _statusBarHeight + 44.0f - navigationBarHeight)); + if (ABS(_statusBarOffset - statusBarOffset) > FLT_EPSILON) { + _statusBarOffset = statusBarOffset; + if (_statusBarOffsetUpdated) { + _statusBarOffsetUpdated(statusBarOffset); + } + + _scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(_navigationBar.bounds.size.height, 0.0f, 0.0f, 0.0f); + }; + };*/ + + var transition: ContainedViewLayoutTransition = .immediate + var navigationBarFrame = self.navigationBar.frame + navigationBarFrame.size.width = bounds.size.width + if navigationBarFrame.size.height.isZero { + navigationBarFrame.size.height = 64.0 + } + if forceState { + transition = .animated(duration: 0.3, curve: .spring) + + if contentOffset.y <= -self.scrollNode.view.contentInset.top || CGFloat(32.0).isLess(than: navigationBarFrame.size.height) { + navigationBarFrame.size.height = 64.0 + } else { + navigationBarFrame.size.height = 20.0 + } + } else { + if contentOffset.y <= -self.scrollNode.view.contentInset.top { + navigationBarFrame.size.height = 64.0 + } else { + navigationBarFrame.size.height -= delta + } + navigationBarFrame.size.height = max(20.0, min(64.0, navigationBarFrame.size.height)) + } + + if navigationBarFrame.height.isEqual(to: 64.0) { + assert(true) + } + + let statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0)) + transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha) + self.statusBar.verticalOffset = navigationBarFrame.size.height - 64.0 + + transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame) + self.navigationBar.updateLayout(size: navigationBarFrame.size, transition: transition) + + transition.animateView { + self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: 0.0, right: 0.0) + } + + /*CGFloat progress = 0.0f; + if (_scrollView.contentSize.height > FLT_EPSILON) { + progress = MAX(0.0f, MIN(1.0f, (_scrollView.contentOffset.y + _scrollView.contentInset.top) / (_scrollView.contentSize.height - _scrollView.frame.size.height + _scrollView.contentInset.top))); + } + [_navigationBar setProgress:progress];*/ } } diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index aa6763da1f..7fe74fe270 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -23,10 +23,10 @@ final class InstantPageLayout { } } -func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int) -> InstantPageLayout { +func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { switch block { case let .cover(block): - return layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter) + return layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) case let .title(text): let styleStack = InstantPageTextStyleStack() styleStack.push(.fontSize(28.0)) @@ -41,38 +41,6 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case let .header(text): - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(24.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case let .subheader(text): - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(19.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case let .paragraph(text): - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case let .preformatted(text): - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(16.0)) - styleStack.push(.fontFixed(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let backgroundInset: CGFloat = 14.0 - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset) - let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: UIColor(rgb: 0xF5F8FC)) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [backgroundItem, item]) case let .authorDate(author: author, date: date): let styleStack = InstantPageTextStyleStack() styleStack.push(.fontSize(15.0)) @@ -112,19 +80,206 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h if let text = text { let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) + + if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL { + item.alignment = .right + } + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) } else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } + case let .header(text): + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(24.0)) + styleStack.push(.fontSerif(true)) + styleStack.push(.lineSpacingFactor(0.685)) + let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case let .subheader(text): + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(19.0)) + styleStack.push(.fontSerif(true)) + styleStack.push(.lineSpacingFactor(0.685)) + let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case let .paragraph(text): + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(17.0)) + let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case let .preformatted(text): + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(16.0)) + styleStack.push(.fontFixed(true)) + styleStack.push(.lineSpacingFactor(0.685)) + let backgroundInset: CGFloat = 14.0 + let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0) + item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset) + let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: UIColor(rgb: 0xF5F8FC)) + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0), items: [backgroundItem, item]) + case let .footer(text): + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(15.0)) + let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case .divider: + let lineWidth = floor(boundingWidth / 2.0) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: UIColor(rgb: 0x79828b)) + return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem]) + case let .list(contentItems, ordered): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var maxIndexWidth: CGFloat = 0.0 + var listItems: [InstantPageItem] = [] + var indexItems: [InstantPageItem] = [] + for i in 0 ..< contentItems.count { + if ordered { + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(17.0)) + let textItem = layoutTextItemWithString(attributedStringForRichText(.plain("\(i + 1)."), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + if let line = textItem.lines.first { + maxIndexWidth = max(maxIndexWidth, line.frame.size.width) + } + indexItems.append(textItem) + } else { + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: UIColor.black) + indexItems.append(shapeItem) + } + } + let indexSpacing: CGFloat = ordered ? 7.0 : 20.0 + for i in 0 ..< contentItems.count { + if (i != 0) { + contentSize.height += 20.0 + } + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(17.0)) + + let textItem = layoutTextItemWithString(attributedStringForRichText(contentItems[i], styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth) + textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + indexSpacing + maxIndexWidth, dy: contentSize.height) + + contentSize.height += textItem.frame.size.height + indexItems[i].frame = indexItems[i].frame.offsetBy(dx: horizontalInset, dy: textItem.frame.origin.y) + listItems.append(indexItems[i]) + listItems.append(textItem) + } + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems) + case let .blockQuote(text, caption): + let lineInset: CGFloat = 20.0 + let verticalInset: CGFloat = 4.0 + var contentSize = CGSize(width: boundingWidth, height: verticalInset) + + var items: [InstantPageItem] = [] + + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(17.0)) + styleStack.push(.fontSerif(true)) + styleStack.push(.italic) + + let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset) + textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height) + + contentSize.height += textItem.frame.size.height + items.append(textItem) + + if case .empty = caption { + } else { + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(15.0)) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset) + captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height) + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + contentSize.height += verticalInset + + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: UIColor.black) + + items.append(shapeItem) + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .pullQuote(text, caption): + let verticalInset: CGFloat = 4.0 + var contentSize = CGSize(width: boundingWidth, height: verticalInset) + + var items: [InstantPageItem] = [] + + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(17.0)) + styleStack.push(.fontSerif(true)) + styleStack.push(.italic) + + let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + textItem.frame = textItem.frame.offsetBy(dx: floor(boundingWidth - textItem.frame.size.width) / 2.0, dy: contentSize.height) + textItem.alignment = .center + + contentSize.height += textItem.frame.size.height + items.append(textItem) + + if case .empty = caption { + } else { + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(15.0)) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + contentSize.height += verticalInset + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .image(id, caption): if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageSize = largest.dimensions - let filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0)) + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0)) + + if fillToWidthAndHeight { + filledSize = CGSize(width: boundingWidth, height: boundingWidth) + } else if isCover { + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth, height: 1.0)) + if !filledSize.height.isZero { + filledSize = filledSize.cropped(CGSize(width: boundingWidth, height: floor(boundingWidth * 3.0 / 5.0))) + } + } + let mediaIndex = mediaIndexCounter mediaIndexCounter += 1 - let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: nil), arguments: InstantPageMediaArguments.image(interactive: true, roundCorners: false, fit: false)) - return InstantPageLayout(origin: CGPoint(), contentSize: mediaItem.frame.size, items: [mediaItem]) + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), arguments: InstantPageMediaArguments.image(interactive: true, roundCorners: false, fit: false)) + + items.append(mediaItem) + contentSize.height += filledSize.height + + if case .empty = caption { + } else { + contentSize.height += 10.0 + + let styleStack = InstantPageTextStyleStack() + styleStack.push(.fontSize(15.0)) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) } else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } @@ -146,379 +301,6 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } } - -/* - - if ([block isKindOfClass:[TGInstantPageBlockPhoto class]]) { - TGInstantPageBlockPhoto *photoBlock = (TGInstantPageBlockPhoto *)block; - TGImageMediaAttachment *imageMedia = images[@(photoBlock.photoId)]; - if (imageMedia != nil) { - CGSize imageSize = CGSizeZero; - if ([imageMedia.imageInfo imageUrlForLargestSize:&imageSize] != nil) { - CGSize filledSize = TGFitSize(imageSize, CGSizeMake(boundingWidth, 1200.0)); - if (fillToWidthAndHeight) { - filledSize = CGSizeMake(boundingWidth, boundingWidth); - } else if (isCover) { - filledSize = TGScaleToFill(imageSize, CGSizeMake(boundingWidth, 1.0f)); - if (filledSize.height > FLT_EPSILON) { - filledSize = TGCropSize(filledSize, CGSizeMake(boundingWidth, CGFloor(boundingWidth * 3.0f / 5.0f))); - } - } - - NSMutableArray *items = [[NSMutableArray alloc] init]; - - NSInteger mediaIndex = *mediaIndexCounter; - (*mediaIndexCounter)++; - - CGSize contentSize = CGSizeMake(boundingWidth, 0.0f); - TGImageMediaAttachment *mediaWithCaption = [imageMedia copy]; - mediaWithCaption.caption = richPlainText(photoBlock.caption); - TGInstantPageMediaItem *mediaItem = [[TGInstantPageMediaItem alloc] initWithFrame:CGRectMake(CGFloor((boundingWidth - filledSize.width) / 2.0f), 0.0f, filledSize.width, filledSize.height) media:[[TGInstantPageMedia alloc] initWithIndex:mediaIndex media:mediaWithCaption] arguments:[[TGInstantPageImageMediaArguments alloc] initWithInteractive:true roundCorners:false fit:false]]; - [items addObject:mediaItem]; - contentSize.height += filledSize.height; - - if (photoBlock.caption != nil) { - contentSize.height += 10.0f; - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - TGInstantPageTextItem *captionItem = [self layoutTextItemWithString:[self attributedStringForRichText:photoBlock.caption styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - if (filledSize.width >= boundingWidth - FLT_EPSILON) { - captionItem.alignment = NSTextAlignmentCenter; - captionItem.frame = CGRectOffset(captionItem.frame, horizontalInset, contentSize.height); - } else { - captionItem.alignment = NSTextAlignmentCenter; - captionItem.frame = CGRectOffset(captionItem.frame, CGFloor((boundingWidth - captionItem.frame.size.width) / 2.0), contentSize.height); - } - contentSize.height += captionItem.frame.size.height; - [items addObject:captionItem]; - } - - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:items]; - } - } - } - - */ - - - - - - - - -/*+ - else if ([block isKindOfClass:[TGInstantPageBlockFooter class]]) { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - TGInstantPageTextItem *item = [self layoutTextItemWithString:[self attributedStringForRichText:((TGInstantPageBlockFooter *)block).text styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - item.frame = CGRectOffset(item.frame, horizontalInset, 0.0f); - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:item.frame.size items:@[item]]; - - } else if ([block isKindOfClass:[TGInstantPageBlockDivider class]]) { - CGFloat lineWidth = CGFloor(boundingWidth / 2.0f); - TGInstantPageShapeItem *shapeItem = [[TGInstantPageShapeItem alloc] initWithFrame:CGRectMake(CGFloor((boundingWidth - lineWidth) / 2.0f), 0.0f, lineWidth, 1.0f) shapeFrame:CGRectMake(0.0f, 0.0f, lineWidth, 1.0f) shape:TGInstantPageShapeRect color:UIColorRGBA(0x79828B, 0.4f)]; - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:shapeItem.frame.size items:@[shapeItem]]; - } else if ([block isKindOfClass:[TGInstantPageBlockList class]]) { - TGInstantPageBlockList *listBlock = (TGInstantPageBlockList *)block; - CGSize contentSize = CGSizeMake(boundingWidth, 0.0f); - CGFloat maxIndexWidth = 0.0f; - NSMutableArray> *listItems = [[NSMutableArray alloc] init]; - NSMutableArray> *indexItems = [[NSMutableArray alloc] init]; - for (NSUInteger i = 0; i < listBlock.items.count; i++) { - if (listBlock.ordered) { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:17.0]]; - - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:[[TGRichTextPlain alloc] initWithText:[NSString stringWithFormat:@"%d.", (int)i + 1]] styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - maxIndexWidth = MAX(textItem->_lines.firstObject.frame.size.width, maxIndexWidth); - [indexItems addObject:textItem]; - } else { - TGInstantPageShapeItem *shapeItem = [[TGInstantPageShapeItem alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 6.0f, 12.0f) shapeFrame:CGRectMake(0.0f, 3.0f, 6.0f, 6.0f) shape:TGInstantPageShapeEllipse color:[UIColor blackColor]]; - [indexItems addObject:shapeItem]; - } - } - NSInteger index = -1; - CGFloat indexSpacing = listBlock.ordered ? 7.0f : 20.0f; - for (TGRichText *text in listBlock.items) { - index++; - if (index != 0) { - contentSize.height += 20.0f; - } - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:17.0]]; - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:text styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset - indexSpacing - maxIndexWidth]; - textItem.frame = CGRectOffset(textItem.frame, horizontalInset + indexSpacing + maxIndexWidth, contentSize.height); - - contentSize.height += textItem.frame.size.height; - id indexItem = indexItems[index]; - indexItem.frame = CGRectOffset(indexItem.frame, horizontalInset, textItem.frame.origin.y); - [listItems addObject:indexItem]; - [listItems addObject:textItem]; - } - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:listItems]; - } else if ([block isKindOfClass:[TGInstantPageBlockBlockQuote class]]) { - TGInstantPageBlockBlockQuote *quoteBlock = (TGInstantPageBlockBlockQuote *)block; - CGFloat lineInset = 20.0f; - CGFloat verticalInset = 4.0f; - CGSize contentSize = CGSizeMake(boundingWidth, verticalInset); - - NSMutableArray> *items = [[NSMutableArray alloc] init]; - - { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:17.0]]; - [styleStack pushItem:[[TGInstantPageStyleFontSerifItem alloc] initWithSerif:true]]; - [styleStack pushItem:[[TGInstantPageStyleItalicItem alloc] init]]; - - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:quoteBlock.text styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset - lineInset]; - textItem.frame = CGRectOffset(textItem.frame, horizontalInset + lineInset, contentSize.height); - - contentSize.height += textItem.frame.size.height; - [items addObject:textItem]; - } - if (quoteBlock.caption != nil) { - contentSize.height += 14.0f; - { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - - TGInstantPageTextItem *captionItem = [self layoutTextItemWithString:[self attributedStringForRichText:quoteBlock.caption styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset - lineInset]; - captionItem.frame = CGRectOffset(captionItem.frame, horizontalInset + lineInset, contentSize.height); - - contentSize.height += captionItem.frame.size.height; - [items addObject:captionItem]; - } - } - contentSize.height += verticalInset; - [items addObject:[[TGInstantPageShapeItem alloc] initWithFrame:CGRectMake(horizontalInset, 0.0f, 3.0f, contentSize.height) shapeFrame:CGRectMake(0.0f, 0.0f, 3.0f, contentSize.height) shape:TGInstantPageShapeRoundLine color:[UIColor blackColor]]]; - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:items]; - } else if ([block isKindOfClass:[TGInstantPageBlockPullQuote class]]) { - TGInstantPageBlockPullQuote *quoteBlock = (TGInstantPageBlockPullQuote *)block; - CGFloat verticalInset = 4.0f; - CGSize contentSize = CGSizeMake(boundingWidth, verticalInset); - - NSMutableArray> *items = [[NSMutableArray alloc] init]; - - { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:17.0]]; - [styleStack pushItem:[[TGInstantPageStyleFontSerifItem alloc] initWithSerif:true]]; - [styleStack pushItem:[[TGInstantPageStyleItalicItem alloc] init]]; - - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:quoteBlock.text styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - textItem.frame = CGRectOffset(textItem.frame, CGFloor((boundingWidth - textItem.frame.size.width) / 2.0), contentSize.height); - textItem.alignment = NSTextAlignmentCenter; - - contentSize.height += textItem.frame.size.height; - [items addObject:textItem]; - } - contentSize.height += 14.0f; - { - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - - TGInstantPageTextItem *captionItem = [self layoutTextItemWithString:[self attributedStringForRichText:quoteBlock.caption styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - captionItem.frame = CGRectOffset(captionItem.frame, CGFloor((boundingWidth - captionItem.frame.size.width) / 2.0), contentSize.height); - captionItem.alignment = NSTextAlignmentCenter; - - contentSize.height += captionItem.frame.size.height; - [items addObject:captionItem]; - } - contentSize.height += verticalInset; - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:items]; - } else else if ([block isKindOfClass:[TGInstantPageBlockVideo class]]) { - TGInstantPageBlockVideo *videoBlock = (TGInstantPageBlockVideo *)block; - TGVideoMediaAttachment *videoMedia = videos[@(videoBlock.videoId)]; - if (videoMedia != nil) { - CGSize imageSize = [videoMedia dimensions]; - if (imageSize.width > FLT_EPSILON && imageSize.height > FLT_EPSILON) { - CGSize filledSize = TGFitSize(imageSize, CGSizeMake(boundingWidth, 1200.0)); - if (fillToWidthAndHeight) { - filledSize = CGSizeMake(boundingWidth, boundingWidth); - } else if (isCover) { - filledSize = TGScaleToFill(imageSize, CGSizeMake(boundingWidth, 1.0f)); - if (filledSize.height > FLT_EPSILON) { - filledSize = TGCropSize(filledSize, CGSizeMake(boundingWidth, CGFloor(boundingWidth * 3.0f / 5.0f))); - } - } - - NSMutableArray *items = [[NSMutableArray alloc] init]; - - NSInteger mediaIndex = *mediaIndexCounter; - (*mediaIndexCounter)++; - - CGSize contentSize = CGSizeMake(boundingWidth, 0.0f); - TGVideoMediaAttachment *videoWithCaption = [videoMedia copy]; - videoWithCaption.caption = richPlainText(videoBlock.caption); - videoWithCaption.loopVideo = videoBlock.loop; - TGInstantPageMediaItem *mediaItem = [[TGInstantPageMediaItem alloc] initWithFrame:CGRectMake(CGFloor((boundingWidth - filledSize.width) / 2.0f), 0.0f, filledSize.width, filledSize.height) media:[[TGInstantPageMedia alloc] initWithIndex:mediaIndex media:videoWithCaption] arguments:[[TGInstantPageVideoMediaArguments alloc] initWithInteractive:true autoplay:videoBlock.autoplay || videoBlock.loop]]; - [items addObject:mediaItem]; - contentSize.height += filledSize.height; - - if (videoBlock.caption != nil) { - contentSize.height += 10.0f; - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - TGInstantPageTextItem *captionItem = [self layoutTextItemWithString:[self attributedStringForRichText:videoBlock.caption styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - if (filledSize.width >= boundingWidth - FLT_EPSILON) { - captionItem.alignment = NSTextAlignmentCenter; - captionItem.frame = CGRectOffset(captionItem.frame, horizontalInset, contentSize.height); - } else { - captionItem.alignment = NSTextAlignmentCenter; - captionItem.frame = CGRectOffset(captionItem.frame, CGFloor((boundingWidth - captionItem.frame.size.width) / 2.0), contentSize.height); - } - contentSize.height += captionItem.frame.size.height; - [items addObject:captionItem]; - } - - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:items]; - } - } - } else else if ([block isKindOfClass:[TGInstantPageBlockSlideshow class]]) { - TGInstantPageBlockSlideshow *slideshowBlock = (TGInstantPageBlockSlideshow *)block; - NSMutableArray *medias = [[NSMutableArray alloc] init]; - CGSize contentSize = CGSizeMake(boundingWidth, 0.0f); - for (TGInstantPageBlock *subBlock in slideshowBlock.items) { - if ([subBlock isKindOfClass:[TGInstantPageBlockPhoto class]]) { - TGInstantPageBlockPhoto *photoBlock = (TGInstantPageBlockPhoto *)subBlock; - TGImageMediaAttachment *imageMedia = images[@(photoBlock.photoId)]; - if (imageMedia != nil) { - CGSize imageSize = CGSizeZero; - if ([imageMedia.imageInfo imageUrlForLargestSize:&imageSize] != nil) { - TGImageMediaAttachment *mediaWithCaption = [imageMedia copy]; - mediaWithCaption.caption = richPlainText(photoBlock.caption); - NSInteger mediaIndex = *mediaIndexCounter; - (*mediaIndexCounter)++; - - CGSize filledSize = TGFitSize(imageSize, CGSizeMake(boundingWidth, 1200.0f)); - contentSize.height = MAX(contentSize.height, filledSize.height); - [medias addObject:[[TGInstantPageMedia alloc] initWithIndex:mediaIndex media:imageMedia]]; - } - } - } - } - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:@[[[TGInstantPageSlideshowItem alloc] initWithFrame:CGRectMake(0.0f, 0.0f, boundingWidth, contentSize.height) medias:medias]]]; - } else if ([block isKindOfClass:[TGInstantPageBlockCollage class]]) { - TGInstantPageBlockCollage *collageBlock = (TGInstantPageBlockCollage *)block; - CGFloat spacing = 2.0f; - int itemsPerRow = 3; - CGFloat itemSize = (boundingWidth - spacing * MAX(0, itemsPerRow - 1)) / itemsPerRow; - - NSMutableArray *items = [[NSMutableArray alloc] init]; - - CGPoint nextItemOrigin = CGPointMake(0.0f, 0.0f); - for (TGInstantPageBlock *subBlock in collageBlock.items) { - if (nextItemOrigin.x + itemSize > boundingWidth) { - nextItemOrigin.x = 0.0f; - nextItemOrigin.y += itemSize + spacing; - } - TGInstantPageLayout *subLayout = [self layoutBlock:subBlock boundingWidth:itemSize horizontalInset:0.0f isCover:false fillToWidthAndHeight:true images:images videos:videos mediaIndexCounter:mediaIndexCounter]; - [items addObjectsFromArray:[subLayout flattenedItemsWithOrigin:nextItemOrigin]]; - nextItemOrigin.x += itemSize + spacing; - } - - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:CGSizeMake(boundingWidth, nextItemOrigin.y + itemSize) items:items]; - } else if ([block isKindOfClass:[TGInstantPageBlockEmbedPost class]]) { - TGInstantPageBlockEmbedPost *postBlock = (TGInstantPageBlockEmbedPost *)block; - - CGSize contentSize = CGSizeMake(boundingWidth, 0.0f); - CGFloat lineInset = 20.0f; - CGFloat verticalInset = 4.0f; - CGFloat itemSpacing = 10.0f; - CGFloat avatarInset = 0.0f; - CGFloat avatarVerticalInset = 0.0f; - - contentSize.height += verticalInset; - - NSMutableArray *items = [[NSMutableArray alloc] init]; - - if (postBlock.author.length != 0) { - TGImageMediaAttachment *avatar = postBlock.authorPhotoId == 0 ? nil : images[@(postBlock.authorPhotoId)]; - if (avatar != nil) { - TGInstantPageMediaItem *avatarItem = [[TGInstantPageMediaItem alloc] initWithFrame:CGRectMake(horizontalInset + lineInset + 1.0f, contentSize.height - 2.0f, 50.0f, 50.0f) media:[[TGInstantPageMedia alloc] initWithIndex:-1 media:avatar] arguments:[[TGInstantPageImageMediaArguments alloc] initWithInteractive:false roundCorners:true fit:false]]; - [items addObject:avatarItem]; - avatarInset += 62.0f; - avatarVerticalInset += 6.0f; - if (postBlock.date == 0) { - avatarVerticalInset += 11.0f; - } - } - - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:17.0]]; - [styleStack pushItem:[[TGInstantPageStyleBoldItem alloc] init]]; - - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:[[TGRichTextPlain alloc] initWithText:postBlock.author] styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset - lineInset - avatarInset]; - textItem.frame = CGRectOffset(textItem.frame, horizontalInset + lineInset + avatarInset, contentSize.height + avatarVerticalInset); - - contentSize.height += textItem.frame.size.height + avatarVerticalInset; - [items addObject:textItem]; - } - if (postBlock.date != 0) { - if (items.count != 0) { - contentSize.height += itemSpacing; - } - NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate dateWithTimeIntervalSince1970:postBlock.date] dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterNoStyle]; - - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x838C96)]]; - TGInstantPageTextItem *textItem = [self layoutTextItemWithString:[self attributedStringForRichText:[[TGRichTextPlain alloc] initWithText:dateString] styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset - lineInset - avatarInset]; - textItem.frame = CGRectOffset(textItem.frame, horizontalInset + lineInset + avatarInset, contentSize.height); - contentSize.height += textItem.frame.size.height; - if (textItem != nil) { - [items addObject:textItem]; - } - } - - if (true) { - if (items.count != 0) { - contentSize.height += itemSpacing; - } - - TGInstantPageBlock *previousBlock = nil; - for (TGInstantPageBlock *subBlock in postBlock.blocks) { - TGInstantPageLayout *subLayout = [self layoutBlock:subBlock boundingWidth:boundingWidth - horizontalInset - horizontalInset - lineInset horizontalInset:0.0f isCover:false fillToWidthAndHeight:false images:images videos:videos mediaIndexCounter:mediaIndexCounter]; - CGFloat spacing = spacingBetweenBlocks(previousBlock, subBlock); - NSArray *blockItems = [subLayout flattenedItemsWithOrigin:CGPointMake(horizontalInset + lineInset, contentSize.height + spacing)]; - [items addObjectsFromArray:blockItems]; - contentSize.height += subLayout.contentSize.height + spacing; - previousBlock = subBlock; - } - } - - contentSize.height += verticalInset; - - [items addObject:[[TGInstantPageShapeItem alloc] initWithFrame:CGRectMake(horizontalInset, 0.0f, 3.0f, contentSize.height) shapeFrame:CGRectMake(0.0f, 0.0f, 3.0f, contentSize.height) shape:TGInstantPageShapeRoundLine color:[UIColor blackColor]]]; - - TGRichText *postCaption = postBlock.caption; - - if (postCaption != nil) { - contentSize.height += 14.0f; - TGInstantPageStyleStack *styleStack = [[TGInstantPageStyleStack alloc] init]; - [styleStack pushItem:[[TGInstantPageStyleFontSizeItem alloc] initWithSize:15.0]]; - [styleStack pushItem:[[TGInstantPageStyleTextColorItem alloc] initWithColor:UIColorRGB(0x79828B)]]; - TGInstantPageTextItem *captionItem = [self layoutTextItemWithString:[self attributedStringForRichText:postCaption styleStack:styleStack] boundingWidth:boundingWidth - horizontalInset - horizontalInset]; - captionItem.frame = CGRectOffset(captionItem.frame, horizontalInset, contentSize.height); - contentSize.height += captionItem.frame.size.height; - [items addObject:captionItem]; - } - - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:contentSize items:items]; - } else if ([block isKindOfClass:[TGInstantPageBlockAnchor class]]) { - TGInstantPageBlockAnchor *anchorBlock = (TGInstantPageBlockAnchor *)block; - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:CGSizeMake(0.0f, 0.0f) items:@[[[TGInstantPageAnchorItem alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 0.0f, 0.0f) anchor:anchorBlock.name]]]; - } - - return [[TGInstantPageLayout alloc] initWithOrigin:CGPointMake(0.0f, 0.0f) contentSize:CGSizeMake(0.0f, 0.0f) items:@[]]; -}*/ - func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { @@ -539,10 +321,12 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: } var mediaIndexCounter: Int = 0 + var embedIndexCounter: Int = 0 + let theme = InstantPageTheme() var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter) + let blockLayout = layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift new file mode 100644 index 0000000000..a3242f8121 --- /dev/null +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -0,0 +1,142 @@ +import Foundation +import Display +import AsyncDisplayKit + +private let backArrowImage = UIImage(bundleImageName: "Instant View/BackArrow")?.precomposed() +private let settingsImage = UIImage(bundleImageName: "Instant View/SettingsIcon")?.precomposed() + +final class InstantPageNavigationBar: ASDisplayNode { + private var strings: PresentationStrings + + private var pageProgress: CGFloat = 0.0 + + let pageProgressNode: ASDisplayNode + let backButton: HighlightableButtonNode + let shareButton: HighlightableButtonNode + let settingsButton: HighlightableButtonNode + let scrollToTopButton: HighlightableButtonNode + let arrowNode: ASImageNode + let shareLabel: ASTextNode + var shareLabelSize: CGSize + var shareLabelSmallSize: CGSize + + var back: (() -> Void)? + var share: (() -> Void)? + var settings: (() -> Void)? + + init(strings: PresentationStrings) { + self.strings = strings + + self.pageProgressNode = ASDisplayNode() + self.pageProgressNode.isLayerBacked = true + + self.backButton = HighlightableButtonNode() + self.shareButton = HighlightableButtonNode() + self.settingsButton = HighlightableButtonNode() + self.scrollToTopButton = HighlightableButtonNode() + + self.settingsButton.setImage(settingsImage, for: []) + self.settingsButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0)) + + self.arrowNode = ASImageNode() + self.arrowNode.image = backArrowImage + self.arrowNode.isLayerBacked = true + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + + self.shareLabel = ASTextNode() + self.shareLabel.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(17.0), textColor: UIColor(white: 1.0, alpha: 0.7)) + self.shareLabel.isLayerBacked = true + self.shareLabel.displaysAsynchronously = false + + let shareLabelSmall = ASTextNode() + shareLabelSmall.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(12.0), textColor: UIColor(white: 1.0, alpha: 0.7)) + + self.shareLabelSize = self.shareLabel.measure(CGSize(width: 200.0, height: 100.0)) + self.shareLabelSmallSize = shareLabelSmall.measure(CGSize(width: 200.0, height: 100.0)) + + self.shareLabel.frame = CGRect(origin: CGPoint(), size: self.shareLabelSize) + + super.init() + + self.backgroundColor = .black + + self.backButton.addSubnode(self.arrowNode) + self.shareButton.addSubnode(self.shareLabel) + + self.addSubnode(self.pageProgressNode) + self.addSubnode(self.backButton) + self.addSubnode(self.shareButton) + self.addSubnode(self.scrollToTopButton) + //self.addSubnode(self.settingsButton) + + self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) + self.shareButton.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) + self.settingsButton.addTarget(self, action: #selector(self.settingsPressed), forControlEvents: .touchUpInside) + } + + @objc func backPressed() { + self.back?() + } + + @objc func sharePressed() { + self.share?() + } + + @objc func settingsPressed() { + self.settings?() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.backButton, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: 100.0, height: size.height))) + if let image = arrowNode.image { + let arrowImageSize = image.size + + let arrowHeight: CGFloat + if size.height.isLess(than: 64.0) { + arrowHeight = 9.0 * size.height / 44.0 + 87.0 / 11.0; + } else { + arrowHeight = 21.0 + } + let scaledArrowSize = CGSize(width: arrowImageSize.width * arrowHeight / arrowImageSize.height, height: arrowHeight) + transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: 8.0, y: max(0.0, size.height - 44.0) + floor((min(size.height, 44.0) - scaledArrowSize.height) / 2.0)), size: scaledArrowSize)) + } + + transition.updateFrame(node: shareButton, frame: CGRect(origin: CGPoint(x: size.width - 80.0, y: 0.0), size: CGSize(width: 80.0, height: size.height))) + + let shareImageSize = self.shareLabelSize + let shareSmallImageSize = self.shareLabelSmallSize + let shareHeight: CGFloat + if size.height.isLess(than: 64.0) { + let k = (shareImageSize.height - shareSmallImageSize.height) / 44.0 + let b = shareSmallImageSize.height - k * 20.0; + shareHeight = k * size.height + b + } else { + shareHeight = shareImageSize.height; + } + let shareHeightFactor = shareHeight / shareImageSize.height + transition.updateTransformScale(node: self.shareLabel, scale: shareHeightFactor) + + let scaledShareSize = CGSize(width: shareImageSize.width * shareHeightFactor, height: shareImageSize.height * shareHeightFactor) + let shareLabelCenter = CGPoint(x: 80.0 - 8.0 - scaledShareSize.width / 2.0, y: max(0.0, size.height - 44.0) + min(size.height, 44.0) / 2.0) + transition.updatePosition(node: self.shareLabel, position: shareLabelCenter) + + let alpha = 1.0 - (shareImageSize.height - shareHeight) / (shareImageSize.height - shareSmallImageSize.height) + let diffFactor = shareSmallImageSize.height / shareImageSize.height + let smallSettingsWidth = 44.0 * diffFactor + let offset = smallSettingsWidth / 4.0 + + let spacing = max(4.0, (shareLabelCenter.x - scaledShareSize.width / 2.0) * -1.0 + 4.0) + + let xa = shareLabelCenter.x - scaledShareSize.width / 2.0 + let xb = spacing - (44.0 * shareHeightFactor) / 2.0 + let ya = max(0.0, size.height - 44.0) + let yb = min(size.height, 44.0) / 2.0 - 22.0 - 44.0 / 2.0 + transition.updatePosition(node: self.settingsButton, position: CGPoint(x: xa - xb, y: ya + yb)) + transition.updateTransformScale(node: self.settingsButton, scale: shareHeightFactor) + + transition.updateAlpha(node: self.settingsButton, alpha: alpha) + + transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 100.0, y: 0.0), size: CGSize(width: size.width - 100.0 - 80.0 - 44.0, height: size.height))) + } +} diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 3aea61dcbd..001eb1c6a4 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -26,22 +26,43 @@ final class InstantPageTextLine { final class InstantPageTextItem: InstantPageItem { let lines: [InstantPageTextLine] + let rtlLineIndices: Set let hasLinks: Bool var frame: CGRect var alignment: NSTextAlignment = .left let medias: [InstantPageMedia] = [] let wantsNode: Bool = false + var containsRTL: Bool { + return !self.rtlLineIndices.isEmpty + } + init(frame: CGRect, lines: [InstantPageTextLine]) { self.frame = frame self.lines = lines var hasLinks = false + var index = 0 + var rtlLineIndices = Set() for line in lines { if !line.urlItems.isEmpty { hasLinks = true } + + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + if glyphRuns.count != 0 { + inner: for i in 0 ..< glyphRuns.count { + let runStatus = CTRunGetStatus(glyphRuns[i] as! CTRun) + + if runStatus.contains(.rightToLeft) { + rtlLineIndices.insert(index) + break inner + } + } + } + index += 1 } self.hasLinks = hasLinks + self.rtlLineIndices = rtlLineIndices } func drawInTile(context: CGContext) { @@ -55,7 +76,9 @@ final class InstantPageTextItem: InstantPageItem { let lowerOriginBound = clipRect.maxY + 10.0 let boundsWidth = self.frame.size.width - for line in self.lines { + for i in 0 ..< self.lines.count { + let line = self.lines[i] + let lineFrame = line.frame if lineFrame.maxY < upperOriginBound || lineFrame.minY > lowerOriginBound { continue @@ -64,6 +87,10 @@ final class InstantPageTextItem: InstantPageItem { var lineOrigin = lineFrame.origin if self.alignment == .center { lineOrigin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineOrigin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineOrigin.x = boundsWidth - lineFrame.size.width } context.textPosition = CGPoint(x: lineOrigin.x, y: lineOrigin.y + lineFrame.size.height) @@ -288,17 +315,6 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo lines.append(textLine) - var rightAligned = false - - /*let glyphRuns = CTLineGetGlyphRuns(line) - if CFArrayGetCount(glyphRuns) != 0 { - if (CTRunGetStatus(CFArrayGetValueAtIndex(glyphRuns, 0) as! CTRun).rawValue & CTRunStatus.rightToLeft.rawValue) != 0 { - rightAligned = true - } - }*/ - - //hadRTL |= rightAligned; - currentLineOrigin.x = 0.0; currentLineOrigin.y += fontLineHeight + fontLineSpacing diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift new file mode 100644 index 0000000000..86e2e8824c --- /dev/null +++ b/TelegramUI/InstantPageTheme.swift @@ -0,0 +1,7 @@ +import Foundation + +final class InstantPageTheme { + init() { + + } +} diff --git a/TelegramUI/InstantPageTileNode.swift b/TelegramUI/InstantPageTileNode.swift index 3b3ca9c742..0a41c26767 100644 --- a/TelegramUI/InstantPageTileNode.swift +++ b/TelegramUI/InstantPageTileNode.swift @@ -28,7 +28,7 @@ final class InstantPageTileNode: ASDisplayNode { return InstantPageTileNodeParameters(tile: self.tile) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 54ebbc4043..a515165b69 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -121,9 +121,11 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.listNode = ListView() self.scrollNode = ASScrollNode() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = nil self.isOpaque = false diff --git a/TelegramUI/LanguageSelectionController.swift b/TelegramUI/LanguageSelectionController.swift index 12fd11574a..517152acda 100644 --- a/TelegramUI/LanguageSelectionController.swift +++ b/TelegramUI/LanguageSelectionController.swift @@ -29,7 +29,7 @@ private final class LanguageAccessoryView: UIView { self.check = UIImageView() self.check.image = PresentationResourcesItemList.checkIconImage(theme) - self.indicator = ActivityIndicator(theme: theme) + self.indicator = ActivityIndicator(type: .navigationAccent(theme)) super.init(frame: CGRect()) diff --git a/TelegramUI/LanguageSelectionControllerNode.swift b/TelegramUI/LanguageSelectionControllerNode.swift index 0bfa776e76..73ea7d8ad7 100644 --- a/TelegramUI/LanguageSelectionControllerNode.swift +++ b/TelegramUI/LanguageSelectionControllerNode.swift @@ -7,9 +7,11 @@ final class LanguageSelectionControllerNode: ASDisplayNode { var dismiss: (() -> Void)? override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white } diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index d27026b34e..72638dfabf 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -1,33 +1,34 @@ import Foundation import UIKit -import TelegramLegacyComponents +import LegacyComponents import Display import SwiftSignalKit import Postbox +import TelegramCore -func legacyAttachmentMenu(theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { - let controller = TGMenuSheetController() - controller.applicationInterface = parentController.applicationInterface +func legacyAttachmentMenu(account: Account, peer: Peer, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { + let controller = TGMenuSheetController(context: parentController.context, dark: false)! controller.dismissesByOutsideTap = true controller.hasSwipeGesture = true controller.maxHeight = 445.0 - TGMenuSheetButtonItemViewHeight var itemViews: [Any] = [] - - let carouselItem = TGAttachmentCarouselItemView(camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType)! - carouselItem.presentOverlayController = { controller in - return presentOverlayController(controller!) - } + let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: false)! + carouselItem.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) + carouselItem.recipientName = peer.displayTitle carouselItem.cameraPressed = { [weak controller] cameraView in if let controller = controller { openCamera(cameraView, controller) } } + if peer is TelegramUser || peer is TelegramSecretChat { + carouselItem.hasTimer = true + } carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles in if let controller = controller, let carouselItem = carouselItem { controller.dismiss(animated: true) let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent - let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator()) + let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) sendMessagesWithSignals(signals) } }; @@ -85,7 +86,3 @@ func legacyAttachmentMenu(theme: PresentationTheme, strings: PresentationStrings return controller } - -func legacyFileAttachmentMenu(menuSheetController: TGMenuSheetController) { - -} diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index a3c4f201fa..9bc4040f92 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -1,52 +1,45 @@ import Foundation -import TelegramLegacyComponents +import LegacyComponents import Display import UIKit +import TelegramCore +import Postbox -func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { +func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { + let legacyController = LegacyController(presentation: .custom) + legacyController.statusBar.statusBarStyle = .Hide + let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { - controller = TGCameraController(camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) + controller = TGCameraController(context: legacyController.context, saveEditedPhotos: true, saveCapturedMedia: true, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) } else { controller = TGCameraController() } controller.isImportant = true controller.shouldStoreCapturedAssets = true - controller.allowCaptions = false//true + controller.allowCaptions = true controller.inhibitDocumentCaptions = false - controller.suggestionContext = nil + controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) + controller.recipientName = peer.displayTitle + if peer is TelegramUser || peer is TelegramSecretChat { + controller.hasTimer = true + } let screenSize = parentController.view.bounds.size - var standalone = true var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height) if let cameraView = cameraView, let menuController = menuController { - standalone = false startFrame = menuController.view.convert(cameraView.previewView()!.frame, from: cameraView) } - let legacyController = LegacyController(legacyController: controller, presentation: .custom) - legacyController.controllerLoaded = { [weak controller, weak legacyController] in - if let controller = controller, let legacyController = legacyController { + legacyController.bind(controller: controller) + legacyController.controllerLoaded = { [weak controller] in + if let controller = controller { cameraView?.detachPreviewView() controller.beginTransitionIn(from: startFrame) } } - controller.presentOverlayController = { [weak legacyController] controller in - if let legacyController = legacyController { - let childController = LegacyController(legacyController: controller!, presentation: .custom) - legacyController.present(childController, in: .window(.root)) - return { [weak childController] in - childController?.dismiss() - } - } else { - return { - - } - } - } - controller.beginTransitionOut = { [weak controller, weak cameraView] in if let controller = controller, let cameraView = cameraView { cameraView.willAttachPreviewView() @@ -56,30 +49,31 @@ func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: } } - controller.finishedTransitionOut = { [weak cameraView] in + controller.finishedTransitionOut = { [weak cameraView, weak legacyController] in if let cameraView = cameraView { cameraView.attachPreviewView(animated: true) } - } - - controller.customDismiss = { [weak legacyController] in legacyController?.dismiss() } - controller.finishedWithPhoto = { [weak menuController] image, caption, stickers in + controller.finishedWithPhoto = { [weak menuController, weak legacyController] overlayController, image, caption, stickers, timer in if let image = image { let description = NSMutableDictionary() description["type"] = "capturedPhoto" description["image"] = image + if let timer = timer { + description["timer"] = timer + } if let item = legacyAssetPickerItemGenerator()(description, caption, nil) { sendMessagesWithSignals([SSignal.single(item)]) } } menuController?.dismiss(animated: false) + legacyController?.dismissWithAnimation() } - controller.finishedWithVideo = { [weak menuController] videoURL, previewImage, duration, dimensions, adjustments, caption, stickers in + controller.finishedWithVideo = { [weak menuController, weak legacyController] overlayController, videoURL, previewImage, duration, dimensions, adjustments, caption, stickers, timer in if let videoURL = videoURL { let description = NSMutableDictionary() description["type"] = "video" @@ -92,90 +86,16 @@ func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: } description["duration"] = duration as NSNumber description["dimensions"] = NSValue(cgSize: dimensions) + if let timer = timer { + description["timer"] = timer + } if let item = legacyAssetPickerItemGenerator()(description, caption, nil) { sendMessagesWithSignals([SSignal.single(item)]) } } menuController?.dismiss(animated: false) + legacyController?.dismissWithAnimation() } parentController.present(legacyController, in: .window(.root)) - - /* - - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) - controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); - - __weak TGModernConversationController *weakSelf = self; - __weak TGCameraController *weakCameraController = controller; - __weak TGAttachmentCameraView *weakCameraView = cameraView; - - controller.beginTransitionOut = ^CGRect - { - __strong TGCameraController *strongCameraController = weakCameraController; - if (strongCameraController == nil) - return CGRectZero; - - __strong TGAttachmentCameraView *strongCameraView = weakCameraView; - if (strongCameraView != nil) - { - [strongCameraView willAttachPreviewView]; - if (TGIsPad()) - return CGRectZero; - - return [strongCameraController.view convertRect:strongCameraView.frame fromView:strongCameraView.superview]; - } - - return CGRectZero; - }; - - controller.finishedTransitionOut = ^ - { - __strong TGAttachmentCameraView *strongCameraView = weakCameraView; - if (strongCameraView == nil) - return; - - [strongCameraView attachPreviewViewAnimated:true]; - }; - - controller.finishedWithPhoto = ^(UIImage *resultImage, NSString *caption, NSArray *stickers) - { - __strong TGModernConversationController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - __autoreleasing NSString *disabledMessage = nil; - if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) - { - [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; - return; - } - - NSDictionary *imageDescription = [strongSelf->_companion imageDescriptionFromImage:resultImage stickers:stickers caption:caption optionalAssetUrl:nil]; - NSMutableArray *descriptions = [[NSMutableArray alloc] init]; - if (imageDescription != nil) - [descriptions addObject:imageDescription]; - [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:descriptions asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; - - [menuController dismissAnimated:false]; - }; - - controller.finishedWithVideo = ^(NSURL *videoURL, UIImage *previewImage, NSTimeInterval duration, CGSize dimensions, TGVideoEditAdjustments *adjustments, NSString *caption, NSArray *stickers) - { - __strong TGModernConversationController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - __autoreleasing NSString *disabledMessage = nil; - if (![TGApplicationFeatures isFileUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) - { - [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; - return; - } - - NSDictionary *desc = [strongSelf->_companion videoDescriptionFromVideoURL:videoURL previewImage:previewImage dimensions:dimensions duration:duration adjustments:adjustments stickers:stickers caption:caption]; - [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:@[ desc ] asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; - - [menuController dismissAnimated:true]; - };*/ } diff --git a/TelegramUI/LegacyComponentsStickers.swift b/TelegramUI/LegacyComponentsStickers.swift new file mode 100644 index 0000000000..1fbad493d7 --- /dev/null +++ b/TelegramUI/LegacyComponentsStickers.swift @@ -0,0 +1,159 @@ +import Foundation +import LegacyComponents +import Postbox +import TelegramCore +import SwiftSignalKit + +func legacyComponentsStickers(postbox: Postbox, namespace: Int32) -> SSignal { + return SSignal { subscriber in + let disposable = (postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [namespace], aroundIndex: nil, count: 1000)).start(next: { view in + var stickerPackDocuments: [ItemCollectionId: [Any]] = [:] + + for entry in view.entries { + if let item = entry.item as? StickerPackItem { + if stickerPackDocuments[entry.index.collectionId] == nil { + stickerPackDocuments[entry.index.collectionId] = [] + } + let document = TGDocumentMediaAttachment() + document.documentId = item.file.fileId.id + if let resource = item.file.resource as? CloudDocumentMediaResource { + document.accessHash = resource.accessHash + document.datacenterId = Int32(resource.datacenterId) + } + document.mimeType = item.file.mimeType + if let size = item.file.size { + document.size = Int32(size) + } + if let thumbnail = item.file.previewRepresentations.first { + let imageInfo = TGImageInfo() + imageInfo.addImage(with: thumbnail.dimensions, url: "\(thumbnail.resource)") + } + var attributes: [Any] = [] + for attribute in item.file.attributes { + switch attribute { + case let .Sticker(displayText, _, maskData): + attributes.append(TGDocumentAttributeSticker(alt: displayText, packReference: nil, mask: maskData.flatMap { + return TGStickerMaskDescription(n: $0.n, point: CGPoint(x: CGFloat($0.x), y: CGFloat($0.y)), zoom: CGFloat($0.zoom)) + })) + default: + break + } + } + document.attributes = attributes + stickerPackDocuments[entry.index.collectionId]!.append(document) + } + } + + let stickerPacks = NSMutableArray() + for (id, info, _) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + let pack = TGStickerPack(packReference: TGStickerPackIdReference(), title: info.title, stickerAssociations: [], documents: stickerPackDocuments[id] ?? [], packHash: info.hash, hidden: false, isMask: true)! + stickerPacks.add(pack) + } + } + + var dict: [AnyHashable: Any] = [:] + dict["packs"] = stickerPacks + subscriber?.putNext(dict) + }) + + return SBlockDisposable { + disposable.dispose() + } + } +} + +private final class LegacyStickerImageDataTask: NSObject { + private let disposable = DisposableSet() + + init(account: Account, file: TelegramMediaFile, small: Bool, fitSize: CGSize, completion: @escaping (UIImage?) -> Void) { + super.init() + + self.disposable.add(chatMessageLegacySticker(account: account, file: file, small: small, fitSize: fitSize, fetched: true, onlyFullSize: true).start(next: { generator in + if let image = generator(TransformImageArguments(corners: ImageCorners(), imageSize: fitSize, boundingSize: fitSize, intrinsicInsets: UIEdgeInsets()))?.generateImage() { + completion(image) + } + })) + } + + deinit { + self.disposable.dispose() + } + + func cancel() { + self.disposable.dispose() + } +} + +private let sharedImageCache = TGMemoryImageCache(softMemoryLimit: 2 * 1024 * 1024, hardMemoryLimit: 3 * 1024 * 1024)! + +final class LegacyStickerImageDataSource: TGImageDataSource { + private let account: () -> Account? + + init(account: @escaping () -> Account?) { + self.account = account + + super.init() + } + + override func canHandleUri(_ uri: String!) -> Bool { + if let uri = uri { + if uri.hasPrefix("sticker-preview://") { + return true + } else if uri.hasPrefix("sticker://") { + return true + } + } + return false + } + + override func loadDataSync(withUri uri: String!, canWait: Bool, acceptPartialData: Bool, asyncTaskId: AutoreleasingUnsafeMutablePointer!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> TGDataResource! { + if let image = sharedImageCache.image(forKey: uri, attributes: nil) { + return TGDataResource(image: image, decoded: true) + } + return nil + } + + override func loadDataAsync(withUri uri: String!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> Any! { + if let account = self.account() { + let args: [AnyHashable : Any] + let highQuality: Bool + if uri.hasPrefix("sticker-preview://") { + let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "sticker-preview://?".characters.count)) + args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! + highQuality = Int((args["highQuality"] as! String))! != 0 + } else if uri.hasPrefix("sticker://") { + let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "sticker://?".characters.count)) + args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! + highQuality = true + } else { + return nil + } + + let documentId = Int64((args["documentId"] as! String))! + let datacenterId = Int((args["datacenterId"] as! String))! + let accessHash = Int64((args["accessHash"] as! String))! + let size: Int? = nil + + let width = Int((args["width"] as! String))! + let height = Int((args["height"] as! String))! + + let fitSize = CGSize(width: CGFloat(width), height: CGFloat(height)) + + return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: []), small: !highQuality, fitSize: fitSize, completion: { image in + if let image = image { + sharedImageCache.setImage(image, forKey: uri, attributes: nil) + completion?(TGDataResource(image: image, decoded: true)) + } + }) + } else { + return nil + } + } + + override func cancelTask(byId taskId: Any!) { + if let task = taskId as? LegacyStickerImageDataTask { + task.cancel() + } + } +} diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index c315f3391c..8adde9434b 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -1,6 +1,6 @@ import Foundation import Display -import TelegramLegacyComponents +import LegacyComponents public enum LegacyControllerPresentation { case custom @@ -20,7 +20,46 @@ private func passControllerAppearanceAnimated(in: Bool, presentation: LegacyCont } } -private final class LegacyControllerApplicationInterface: NSObject, TGLegacyApplicationInterface { +private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyComponentsOverlayWindowManager { + private weak var contentController: UIViewController? + private weak var parentController: ViewController? + private var controller: LegacyController? + private var boundController = false + + init(parentController: ViewController?) { + self.parentController = parentController + self.controller = LegacyController(presentation: .custom) + + super.init() + } + + func managesWindow() -> Bool { + return true + } + + func bindController(_ controller: UIViewController!) { + self.contentController = controller + } + + func context() -> LegacyComponentsContext! { + return self.controller?.context + } + + func setHidden(_ hidden: Bool, window: UIWindow!) { + if hidden { + self.controller?.dismiss() + self.controller = nil + } else if let contentController = self.contentController, let parentController = self.parentController, let controller = self.controller { + if !self.boundController { + controller.bind(controller: contentController) + self.boundController = true + } + parentController.present(controller, in: .window(.root)) + } + } +} + +final class LegacyControllerContext: NSObject, LegacyComponentsContext { private weak var controller: ViewController? init(controller: ViewController?) { @@ -29,14 +68,38 @@ private final class LegacyControllerApplicationInterface: NSObject, TGLegacyAppl super.init() } - @available(iOS 8.0, *) - public func currentSizeClass() -> UIUserInterfaceSizeClass { - return .compact + public func fullscreenBounds() -> CGRect { + if let controller = self.controller { + return controller.view.bounds + } else { + return CGRect() + } } - @available(iOS 8.0, *) - public func currentHorizontalSizeClass() -> UIUserInterfaceSizeClass { - return .compact + public func keyCommandController() -> TGKeyCommandController! { + return nil + } + + public func rootCallStatusBarHidden() -> Bool { + return true + } + + public func statusBarFrame() -> CGRect { + return legacyComponentsApplication!.statusBarFrame + } + + public func isStatusBarHidden() -> Bool { + if let controller = self.controller { + return controller.statusBar.isHidden + } else { + return true + } + } + + public func setStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation) { + if let controller = self.controller { + controller.statusBar.isHidden = hidden + } } public func forceSetStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation) { @@ -45,83 +108,138 @@ private final class LegacyControllerApplicationInterface: NSObject, TGLegacyAppl } } - public func applicationBounds() -> CGRect { - if let controller = controller { - return controller.view.bounds; + public func statusBarStyle() -> UIStatusBarStyle { + if let controller = self.controller { + switch controller.statusBar.statusBarStyle { + case .Black: + return .default + case .White: + return .lightContent + default: + return .default + } } else { - return CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 320.0, height: 480.0)) + return .default } } - public func applicationStatusBarAlpha() -> CGFloat { - return controller?.statusBar.alpha ?? 1.0 + public func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle, animated: Bool) { + if let controller = self.controller { + switch statusBarStyle { + case .default: + controller.statusBar.statusBarStyle = .Black + case .lightContent: + controller.statusBar.statusBarStyle = .White + default: + controller.statusBar.statusBarStyle = .Black + } + } } - public func setApplicationStatusBarAlpha(_ alpha: CGFloat) { - controller?.statusBar.alpha = alpha + public func forceStatusBarAppearanceUpdate() { } - public func applicationStatusBarOffset() -> CGFloat { + public func currentlyInSplitView() -> Bool { + return false + } + + public func currentSizeClass() -> UIUserInterfaceSizeClass { + return .compact + } + + public func currentHorizontalSizeClass() -> UIUserInterfaceSizeClass { + return .compact + } + + public func currentVerticalSizeClass() -> UIUserInterfaceSizeClass { + return .compact + } + + public func sizeClassSignal() -> SSignal! { + return SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber) + } + + public func canOpen(_ url: URL!) -> Bool { + return false + } + + public func open(_ url: URL!) { + } + + public func serverMediaData(forAssetUrl url: String!) -> [AnyHashable : Any]! { + return nil + } + + public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Swift.Void)!) { + } + + func makeOverlayWindowManager() -> LegacyComponentsOverlayWindowManager! { + return LegacyComponentsOverlayWindowManagerImpl(parentController: self.controller) + } + + func applicationStatusBarAlpha() -> CGFloat { + if let controller = self.controller { + return controller.statusBar.alpha + } return 0.0 } - public func setApplicationStatusBarOffset(_ offset: CGFloat) { - - } - - public func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, delay: TimeInterval, duration: TimeInterval, completion: (() -> Swift.Void)!) { - if let completion = completion { - completion() + func setApplicationStatusBarAlpha(_ alpha: CGFloat) { + if let controller = self.controller { + controller.statusBar.alpha = alpha } } - - public func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, duration: TimeInterval, completion: (() -> Swift.Void)!) { - if let completion = completion { - completion() - } + + func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, delay: TimeInterval, duration: TimeInterval, completion: (() -> Void)!) { + completion?() } - public func animateApplicationStatusBarStyleTransition(withDuration duration: TimeInterval) { - + func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, duration: TimeInterval, completion: (() -> Void)!) { + self.animateApplicationStatusBarAppearance(statusBarAnimation, delay: 0.0, duration: duration, completion: completion) } - public func makeOverlayControllerWindow(_ parentController: TGViewController!, contentController: TGOverlayController!, keepKeyboard: Bool) -> TGOverlayControllerWindow! { - return LegacyOverlayWindowHost(presentInWindow: { [weak self] c in - self?.controller?.present(c, in: .window(.root)) - }, parentController: parentController, contentController: contentController, keepKeyboard: keepKeyboard) + func animateApplicationStatusBarStyleTransition(withDuration duration: TimeInterval) { } } public class LegacyController: ViewController { - private let legacyController: UIViewController + private var legacyController: UIViewController! private let presentation: LegacyControllerPresentation private var controllerNode: LegacyControllerNode { return self.displayNode as! LegacyControllerNode } - var applicationInterface: TGLegacyApplicationInterface { - return LegacyControllerApplicationInterface(controller: self) + private var contextImpl: LegacyControllerContext! + public var context: LegacyComponentsContext { + return self.contextImpl! } var controllerLoaded: (() -> Void)? public var presentationCompleted: (() -> Void)? - public init(legacyController: UIViewController, presentation: LegacyControllerPresentation) { - self.legacyController = legacyController + public init(presentation: LegacyControllerPresentation) { self.presentation = presentation super.init(navigationBarTheme: nil) - if let legacyController = legacyController as? TGLegacyApplicationInterfaceHolder { - legacyController.applicationInterface = self.applicationInterface - } + let contextImpl = LegacyControllerContext(controller: self) + self.contextImpl = contextImpl } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + public func bind(controller: UIViewController) { + self.legacyController = controller + if let controller = controller as? TGViewController { + controller.customRemoveFromParentViewController = { [weak self] in + self?.dismiss() + } + } + } + override public func loadDisplayNode() { self.displayNode = LegacyControllerNode() self.displayNodeDidLoad() @@ -181,19 +299,25 @@ public class LegacyController: ViewController { case .modal: self.controllerNode.animateModalOut { [weak self] in if let controller = self?.legacyController as? TGViewController { - controller.didDismiss() + //controller.didDismiss() } else if let controller = self?.legacyController as? TGNavigationController { - controller.didDismiss() + //controller.didDismiss() } self?.presentingViewController?.dismiss(animated: false, completion: completion) } case .custom: if let controller = self.legacyController as? TGViewController { - controller.didDismiss() + //controller.didDismiss() } else if let controller = self.legacyController as? TGNavigationController { - controller.didDismiss() + //controller.didDismiss() } self.presentingViewController?.dismiss(animated: false, completion: completion) } } + + func dismissWithAnimation() { + self.controllerNode.animateModalOut { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + } } diff --git a/TelegramUI/LegacyControllerNode.swift b/TelegramUI/LegacyControllerNode.swift index 9d93f6cf67..63c826e634 100644 --- a/TelegramUI/LegacyControllerNode.swift +++ b/TelegramUI/LegacyControllerNode.swift @@ -14,9 +14,11 @@ final class LegacyControllerNode: ASDisplayNode { } override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.clipsToBounds = true } diff --git a/TelegramUI/LegacyEmptyController.swift b/TelegramUI/LegacyEmptyController.swift index ad2f2da17c..068b738847 100644 --- a/TelegramUI/LegacyEmptyController.swift +++ b/TelegramUI/LegacyEmptyController.swift @@ -1,54 +1,18 @@ import Foundation -import TelegramLegacyComponents +import LegacyComponents import Display final class LegacyEmptyController: TGViewController { - override func viewDidLoad() { - self.view.backgroundColor = nil - self.view.isOpaque = false - } -} - -final class LegacyOverlayWindowHost: TGOverlayControllerWindow { - private let presentInWindow: (ViewController) -> Void - private let content: LegacyController - - init(presentInWindow: @escaping (ViewController) -> Void, parentController: TGViewController!, contentController: TGOverlayController!, keepKeyboard: Bool) { - self.content = LegacyController(legacyController: contentController, presentation: .custom) - self.presentInWindow = presentInWindow - - super.init(parentController: parentController, contentController: contentController, keepKeyboard: keepKeyboard) - - self.rootViewController = nil + override init!(context: LegacyComponentsContext!) { + super.init(context: context) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - if !self._isHidden { - self.content.dismiss() - } - } - - private var _isHidden = true - override var isHidden: Bool { - get { - return self._isHidden - } set(value) { - if value != self._isHidden { - self._isHidden = value - if !value { - self.presentInWindow(self.content) - } else { - self.content.dismiss() - } - } - } - } - - override func dismiss() { - self.isHidden = true + override func viewDidLoad() { + self.view.backgroundColor = nil + self.view.isOpaque = false } } diff --git a/TelegramUI/LegacyImageDownloadActor.swift b/TelegramUI/LegacyImageDownloadActor.swift new file mode 100644 index 0000000000..548afbf495 --- /dev/null +++ b/TelegramUI/LegacyImageDownloadActor.swift @@ -0,0 +1,77 @@ +import Foundation +import LegacyComponents +import Postbox +import TelegramCore +import SwiftSignalKit + +final class LegacyImageDownloadActor: ASActor { + private let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + + override static func genericPath() -> String! { + return "/img/@"; + } + + override func execute(_ options: [AnyHashable : Any]!) { + let actualPath = self.path as NSString + + var url: String + var processor: TGImageProcessor? + var cacheUrl: String + if actualPath.hasPrefix("/img/({filter:") { + let range = actualPath.range(of: "}") + if range.location == NSNotFound { + ActionStageInstance().nodeRetrieveFailed(self.path) + return + } + + let processorName = actualPath.substring(with: NSMakeRange(14, range.location - 14)) + processor = TGRemoteImageView.imageProcessor(forName: processorName) + url = actualPath.substring(with: NSMakeRange(range.location + 1, actualPath.length - range.location - 1 - 1)) + cacheUrl = "{filter:\(processorName)}\(url)" + } + else { + url = actualPath.substring(with: NSMakeRange(6, actualPath.length - 6 - 1)) + cacheUrl = url + } + + if url.hasPrefix("placeholder://") { + let path = self.path + let token = TGImageManager.instance().beginLoadingImageAsync(withUri: url, decode: true, progress: nil, partialCompletion: nil, completion: { image in + ActionStageInstance().actionCompleted(path, result: SGraphObjectNode(object: image)) + }) + let disposable = ActionDisposable { + TGImageManager.instance().cancelTask(withId: token) + } + self.disposable.set(disposable) + } else if let resource = resourceFromLegacyImageUri(url) { + let disposables = DisposableSet() + self.disposable.set(disposables) + if let account = legacyAccountGet() { + let path = self.path + disposables.add(account.postbox.mediaBox.resourceData(resource).start(next: { data in + if data.complete { + ActionStageInstance().globalStageDispatchQueue().async { + if let image = UIImage(contentsOfFile: data.path) { + var updatedImage: UIImage? = image + if let processor = processor { + updatedImage = processor(image) + } + TGRemoteImageView.sharedCache().cacheImage(updatedImage, with: nil, url: cacheUrl, availability: Int32(TGCacheMemory.rawValue)) + ActionStageInstance().actionCompleted(path, result: SGraphObjectNode(object: updatedImage)) + } + } + } + })) + disposables.add(account.postbox.mediaBox.fetchedResource(resource, tag: nil).start()) + } + } + } + + override func cancel() { + self.disposable.dispose() + } +} diff --git a/TelegramUI/LegacyImageProcessors.h b/TelegramUI/LegacyImageProcessors.h new file mode 100644 index 0000000000..6288d94d56 --- /dev/null +++ b/TelegramUI/LegacyImageProcessors.h @@ -0,0 +1,13 @@ +// +// LegacyImageProcessors.h +// TelegramUI +// +// Created by Peter on 8/1/17. +// Copyright © 2017 Telegram. All rights reserved. +// + +#import + +@interface LegacyImageProcessors : NSObject + +@end diff --git a/TelegramUI/LegacyImageProcessors.m b/TelegramUI/LegacyImageProcessors.m new file mode 100644 index 0000000000..dab0029030 --- /dev/null +++ b/TelegramUI/LegacyImageProcessors.m @@ -0,0 +1,56 @@ +#import "LegacyImageProcessors.h" + +#import + +@implementation LegacyImageProcessors + ++ (void)load { + [TGRemoteImageView registerImageUniversalProcessor:^UIImage *(NSString *name, UIImage *source) { + CGSize size = CGSizeZero; + int n = 7; + bool invalid = false; + for (int i = n; i < (int)name.length; i++) { + unichar c = [name characterAtIndex:i]; + if (c == 'x') + { + if (i == n) + invalid = true; + else + { + size.width = [[name substringWithRange:NSMakeRange(n, i - n)] intValue]; + n = i + 1; + } + break; + } + else if (c < '0' || c > '9') + { + invalid = true; + break; + } + } + if (!invalid) + { + for (int i = n; i < (int)name.length; i++) + { + unichar c = [name characterAtIndex:i]; + if (c < '0' || c > '9') + { + invalid = true; + break; + } + else if (i == (int)name.length - 1) + { + size.height = [[name substringFromIndex:n] intValue]; + } + } + } + if (!invalid) + { + return TGScaleAndRoundCornersWithOffsetAndFlags(source, size, CGPointZero, size, (int)size.width / 2, nil, false, nil, TGScaleImageScaleSharper); + } + + return nil; + } withBaseName:@"circle"]; +} + +@end diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift index f84d5161f9..1fc268325a 100644 --- a/TelegramUI/LegacyLocationController.swift +++ b/TelegramUI/LegacyLocationController.swift @@ -1,6 +1,6 @@ import Foundation import Display -import TelegramLegacyComponents +import LegacyComponents import TelegramCore import Postbox @@ -25,13 +25,15 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id) } - let controller = TGLocationViewController(locationAttachment: legacyLocation, peer: legacyPeer)! + let legacyController = LegacyController(presentation: .modal(animateIn: true)) + let controller = TGLocationViewController(context: legacyController.context, locationAttachment: legacyLocation, peer: legacyPeer)! + controller.modalMode = true let navigationController = TGNavigationController(controllers: [controller])! - let legacyController = LegacyController(legacyController: navigationController, presentation: .modal(animateIn: true)) - controller.customDismiss = { [weak legacyController] in + legacyController.bind(controller: navigationController) + controller.navigation_setDismiss({ [weak legacyController] in legacyController?.dismiss() - } - controller.customActions = { [weak legacyController] in + }, rootController: nil) + /*controller.shareAction = { [weak legacyController] in if let legacyController = legacyController { var shareAction: (([PeerId]) -> Void)? let shareController = ShareController(account: account, shareAction: { peerIds in @@ -46,7 +48,7 @@ func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, acco } } } - } + }*/ controller.calloutPressed = { [weak legacyController] in legacyController?.dismiss() diff --git a/TelegramUI/LegacyLocationPicker.swift b/TelegramUI/LegacyLocationPicker.swift index 336f417026..b1039eb7b8 100644 --- a/TelegramUI/LegacyLocationPicker.swift +++ b/TelegramUI/LegacyLocationPicker.swift @@ -1,15 +1,16 @@ import Foundation import Display -import TelegramLegacyComponents +import LegacyComponents import TelegramCore func legacyLocationPickerController(sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void) -> ViewController { - let controller = TGLocationPickerController(intent: TGLocationPickerControllerDefaultIntent)! + let legacyController = LegacyController(presentation: .modal(animateIn: true)) + let controller = TGLocationPickerController(context: legacyController.context, intent: TGLocationPickerControllerDefaultIntent)! let navigationController = TGNavigationController(controllers: [controller])! - let legacyController = LegacyController(legacyController: navigationController, presentation: .modal(animateIn: true)) - controller.customDismiss = { [weak legacyController] in + controller.navigation_setDismiss({ [weak legacyController] in legacyController?.dismiss() - } + }, rootController: nil) + legacyController.bind(controller: navigationController) controller.locationPicked = { [weak legacyController] coordinate, venue in sendLocation(coordinate, venue.flatMap { venue in return MapVenue(title: venue.title, address: venue.address, provider: venue.provider, id: venue.venueId) diff --git a/TelegramUI/LegacyMediaLocations.swift b/TelegramUI/LegacyMediaLocations.swift new file mode 100644 index 0000000000..e97f5ccc3d --- /dev/null +++ b/TelegramUI/LegacyMediaLocations.swift @@ -0,0 +1,42 @@ +import Foundation +import Postbox +import TelegramCore + +func legacyImageLocationUri(resource: MediaResource) -> String? { + if let resource = resource as? CloudFileMediaResource { + return "\(resource.datacenterId)_\(resource.volumeId)_\(resource.localId)_\(resource.secret)" + } + return nil +} + +private let legacyImageUriExpr = try? NSRegularExpression(pattern: "([-\\d]+)_([-\\d]+)_([-\\d]+)_([-\\d]+)", options: []) + +func resourceFromLegacyImageUri(_ uri: String) -> MediaResource? { + guard let legacyImageUriExpr = legacyImageUriExpr else { + return nil + } + let matches = legacyImageUriExpr.matches(in: uri, options: [], range: NSRange(location: 0, length: uri.characters.count)) + if let match = matches.first { + let nsString = uri as NSString + let datacenterId = nsString.substring(with: match.rangeAt(1)) + let volumeId = nsString.substring(with: match.rangeAt(2)) + let localId = nsString.substring(with: match.rangeAt(3)) + let secret = nsString.substring(with: match.rangeAt(4)) + + guard let nDatacenterId = Int(datacenterId) else { + return nil + } + guard let nVolumeId = Int64(volumeId) else { + return nil + } + guard let nLocalId = Int32(localId) else { + return nil + } + guard let nSecret = Int64(secret) else { + return nil + } + + return CloudFileMediaResource(datacenterId: nDatacenterId, volumeId: nVolumeId, localId: nLocalId, secret: nSecret, size: nil) + } + return nil +} diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index 9997b72690..0257d8c688 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -1,5 +1,5 @@ import Foundation -import TelegramLegacyComponents +import LegacyComponents import SwiftSignalKit import TelegramCore import Postbox @@ -7,10 +7,13 @@ import SSignalKit import UIKit import Display -func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false) { +func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: Account, peer: Peer, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false) { controller.captionsEnabled = captionsEnabled controller.inhibitDocumentCaptions = false - controller.suggestionContext = nil + controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) + if peer is TelegramUser || peer is TelegramSecretChat { + controller.hasTimer = true + } controller.dismissalBlock = { } @@ -19,20 +22,18 @@ func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, captionsE controller.shouldShowFileTipIfNeeded = showFileTooltip } -func legacyAssetPicker(fileMode: Bool) -> Signal<(@escaping (UIViewController) -> (() -> Void)) -> TGMediaAssetsController, NoError> { +func legacyAssetPicker(fileMode: Bool, peer: Peer) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, NoError> { return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent if TGMediaAssetsLibrary.authorizationStatus() == TGMediaLibraryAuthorizationStatusNotDetermined { TGMediaAssetsLibrary.requestAuthorization(for: TGMediaAssetAnyType, completion: { (status, group) in - if !TGLegacyComponentsAccessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) { + if !LegacyComponentsGlobals.provider().accessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) { subscriber.putError(NoError()) } else { Queue.mainQueue().async { - subscriber.putNext({ present in - let controller = TGMediaAssetsController(assetGroup: group, intent: intent, presentOverlayController: { controller in - return present(controller!) - }) + subscriber.putNext({ context in + let controller = TGMediaAssetsController(context: context, assetGroup: group, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: true) return controller! }) subscriber.putCompletion() @@ -40,10 +41,8 @@ func legacyAssetPicker(fileMode: Bool) -> Signal<(@escaping (UIViewController) - } }) } else { - subscriber.putNext({ present in - let controller = TGMediaAssetsController(assetGroup: nil, intent: intent, presentOverlayController: { controller in - return present(controller!) - }) + subscriber.putNext({ context in + let controller = TGMediaAssetsController(context: context, assetGroup: nil, intent: intent, recipientName: peer.displayTitle, saveEditedPhotos: true) return controller! }) subscriber.putCompletion() @@ -74,9 +73,11 @@ private enum LegacyAssetItem { private final class LegacyAssetItemWrapper: NSObject { let item: LegacyAssetItem + let timer: Int? - init(item: LegacyAssetItem) { + init(item: LegacyAssetItem, timer: Int?) { self.item = item + self.timer = timer super.init() } @@ -88,7 +89,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { let image = dict["image"] as! UIImage var result: [AnyHashable : Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), caption: caption)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) return result } else if (dict["type"] as! NSString) == "cloudPhoto" { let asset = dict["asset"] as! TGMediaAsset @@ -101,7 +102,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab //result["item" as NSString] = LegacyAssetItemWrapper(item: .file(.asset(asset.backingAsset))) return nil } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) } return result } else if (dict["type"] as! NSString) == "file" { @@ -116,19 +117,19 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) return result } } else if (dict["type"] as! NSString) == "video" { if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) return result } else if let url = dict["url"] as? String { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue) return result } } @@ -158,7 +159,11 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) + var attributes: [MessageAttribute] = [] + if let timer = item.timer, timer > 0 && timer <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) + } + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) } } case let .asset(asset): @@ -169,7 +174,11 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) + var attributes: [MessageAttribute] = [] + if let timer = item.timer, timer > 0 && timer <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) + } + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) case .tempFile: break } @@ -231,7 +240,11 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: } let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [])]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) + var attributes: [MessageAttribute] = [] + if let timer = item.timer, timer > 0 && timer <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) + } + messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil)) } } } diff --git a/TelegramUI/LegacyNavigationController.swift b/TelegramUI/LegacyNavigationController.swift index 836c374e09..fa0c72acd2 100644 --- a/TelegramUI/LegacyNavigationController.swift +++ b/TelegramUI/LegacyNavigationController.swift @@ -1,6 +1,6 @@ import Foundation import UIKit -import TelegramLegacyComponents +import LegacyComponents func makeLegacyNavigationController(rootController: UIViewController) -> TGNavigationController { return TGNavigationController.make(withRootController: rootController) diff --git a/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift b/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift new file mode 100644 index 0000000000..54c5e15902 --- /dev/null +++ b/TelegramUI/LegacyPeerAvatarPlaceholderDataSource.swift @@ -0,0 +1,118 @@ +import Foundation +import LegacyComponents +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor] +] + +private let grayscaleColors: NSArray = [ + UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor +] + +private let sharedImageCache = TGMemoryImageCache(softMemoryLimit: 2 * 1024 * 1024, hardMemoryLimit: 3 * 1024 * 1024)! + +final class LegacyPeerAvatarPlaceholderDataSource: TGImageDataSource { + private let account: () -> Account? + + init(account: @escaping () -> Account?) { + self.account = account + + super.init() + } + + override func canHandleUri(_ uri: String!) -> Bool { + if let uri = uri { + if uri.hasPrefix("placeholder://") { + return true + } + } + return false + } + + override func loadDataSync(withUri uri: String!, canWait: Bool, acceptPartialData: Bool, asyncTaskId: AutoreleasingUnsafeMutablePointer!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> TGDataResource! { + if let image = sharedImageCache.image(forKey: uri, attributes: nil) { + return TGDataResource(image: image, decoded: true) + } + return nil + } + + override func loadDataAsync(withUri uri: String!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> Any! { + 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)) + args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! + + guard let width = Int((args["w"] as! String)), width > 1 else { + return + } + guard let height = Int((args["h"] as! String)), height > 1 else { + return + } + + var peerId = PeerId(namespace: 0, id: 0) + + if let uid = args["uid"] as? String, let nUid = Int32(uid) { + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: nUid) + } + + let image = generateImage(CGSize(width: CGFloat(width), height: CGFloat(height)), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.beginPath() + context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: + size.height)) + context.clip() + + let colorIndex: Int + if peerId.id == 0 { + colorIndex = -1 + } else { + colorIndex = abs(Int(account.peerId.id + peerId.id)) + } + + let colorsArray: NSArray + if colorIndex == -1 { + colorsArray = grayscaleColors + } else { + colorsArray = gradientColors[colorIndex % gradientColors.count] + } + + var locations: [CGFloat] = [1.0, 0.2]; + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context.setBlendMode(.normal) + }) + + sharedImageCache.setImage(image, forKey: uri, attributes: nil) + completion?(TGDataResource(image: image, decoded: true)) + } + + account.graphicsThreadPool.addTask(task) + + return ActionDisposable { + task.cancel() + } + } else { + return nil + } + } + + override func cancelTask(byId taskId: Any!) { + if let disposable = taskId as? Disposable { + disposable.dispose() + } + } +} diff --git a/TelegramUI/LegacySuggestionContext.swift b/TelegramUI/LegacySuggestionContext.swift new file mode 100644 index 0000000000..19e0f2deca --- /dev/null +++ b/TelegramUI/LegacySuggestionContext.swift @@ -0,0 +1,58 @@ +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import LegacyComponents + +func legacySuggestionContext(account: Account, peerId: PeerId) -> TGSuggestionContext { + let context = TGSuggestionContext() + context.userListSignal = { mention in + return SSignal { subscriber in + if let mention = mention { + let normalizedQuery = mention.lowercased() + let disposable = peerParticipants(account: account, id: peerId).start(next: { peers in + let filteredPeers = peers.filter { peer in + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } + return false + } + let sortedPeers = filteredPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) + return result == .orderedAscending + }) + + let users = NSMutableArray() + for peer in sortedPeers { + let user = TGUser() + if let peer = peer as? TelegramUser { + user.uid = peer.id.id + user.firstName = peer.firstName + user.lastName = peer.lastName + user.userName = peer.addressName + if let representation = smallestImageRepresentation(peer.photo) { + user.photoUrlSmall = legacyImageLocationUri(resource: representation.resource) + } + users.add(user) + } + } + + subscriber?.putNext(users) + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } else { + subscriber?.putNext(NSArray()) + subscriber?.putCompletion() + return nil + } + } + } + return context +} diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift index 3a82f96221..417a2ce64a 100644 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -279,6 +279,12 @@ final class ManagedAudioPlaylistPlayer { if file.isInstantVideo { instantVideo = (file, message.id, message.stableId) } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let file = content.file { + if file.isInstantVideo { + instantVideo = (file, message.id, message.stableId) + } + } } } if message.flags.contains(.Incoming) { diff --git a/TelegramUI/MapInputControllerNode.swift b/TelegramUI/MapInputControllerNode.swift index fc100e8d81..ed0e4934e6 100644 --- a/TelegramUI/MapInputControllerNode.swift +++ b/TelegramUI/MapInputControllerNode.swift @@ -15,9 +15,11 @@ final class MapInputControllerNode: ASDisplayNode, MKMapViewDelegate { self.locationManager = CLLocationManager() self.mapView = MKMapView() - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = UIColor.white diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 9b0e001c30..33fa4c5e81 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -65,9 +65,9 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _ in }, navigateToMessage: { _ in }, clickThroughMessage: { }, toggleMessageSelection: { _ 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 }) + }, presentController: { _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, automaticMediaDownloadSettings: .none) - let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .Music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) + let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) listNode.preloadPages = true self.listNode = listNode self.contentNode.addSubnode(listNode) diff --git a/TelegramUI/MediaPlayerTimeTextNode.swift b/TelegramUI/MediaPlayerTimeTextNode.swift index e4ffa880b3..52cc8db051 100644 --- a/TelegramUI/MediaPlayerTimeTextNode.swift +++ b/TelegramUI/MediaPlayerTimeTextNode.swift @@ -123,7 +123,7 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { diff --git a/TelegramUI/MediaTrackDecodableFrame.swift b/TelegramUI/MediaTrackDecodableFrame.swift index db36077da7..72e2073f2a 100644 --- a/TelegramUI/MediaTrackDecodableFrame.swift +++ b/TelegramUI/MediaTrackDecodableFrame.swift @@ -9,25 +9,18 @@ enum MediaTrackFrameType { final class MediaTrackDecodableFrame { let type: MediaTrackFrameType - let packet: UnsafeMutablePointer + let packet: FFMpegPacket let pts: CMTime let dts: CMTime let duration: CMTime - init(type: MediaTrackFrameType, packet: UnsafePointer, pts: CMTime, dts: CMTime, duration: CMTime) { + init(type: MediaTrackFrameType, packet: FFMpegPacket, pts: CMTime, dts: CMTime, duration: CMTime) { self.type = type self.pts = pts self.dts = dts self.duration = duration - self.packet = UnsafeMutablePointer.allocate(capacity: 1) - av_init_packet(self.packet) - av_packet_ref(self.packet, packet) - } - - deinit { - av_packet_unref(self.packet) - self.packet.deallocate(capacity: 1) + self.packet = packet } } diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index 7ebe694bef..b90f8b55bc 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -12,10 +12,10 @@ struct NetworkStatusTitle: Equatable { } } -final class NetworkStatusTitleView: UIView { +final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { private let titleNode: ASTextNode private let lockView: ChatListTitleLockView - private let activityIndicator: UIActivityIndicatorView + private let activityIndicator: ActivityIndicator private let buttonView: HighlightTrackingButton var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false) { @@ -24,11 +24,15 @@ final class NetworkStatusTitleView: UIView { self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) if self.title.activity != oldValue.activity { if self.title.activity { - self.activityIndicator.isHidden = false - self.activityIndicator.startAnimating() + if self.activityIndicator.layer.superlayer == nil { + self.addSubnode(self.activityIndicator) + } + //self.activityIndicator.startAnimating() } else { - self.activityIndicator.isHidden = true - self.activityIndicator.stopAnimating() + if self.activityIndicator.layer.superlayer != nil { + self.activityIndicator.removeFromSupernode() + } + //self.activityIndicator.stopAnimating() } } self.setNeedsLayout() @@ -63,8 +67,9 @@ final class NetworkStatusTitleView: UIView { self.titleNode.isOpaque = false self.titleNode.isUserInteractionEnabled = false - self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) - self.activityIndicator.isHidden = true + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.secondaryTextColor), speed: .slow) + let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize) self.lockView = ChatListTitleLockView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))) self.lockView.isHidden = true @@ -77,7 +82,6 @@ final class NetworkStatusTitleView: UIView { self.addSubview(self.buttonView) self.addSubnode(self.titleNode) self.addSubview(self.lockView) - self.addSubview(self.activityIndicator) self.buttonView.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -112,7 +116,7 @@ final class NetworkStatusTitleView: UIView { var indicatorPadding: CGFloat = 0.0 let indicatorSize = self.activityIndicator.bounds.size - if !self.activityIndicator.isHidden { + if self.activityIndicator.layer.superlayer != nil { indicatorPadding = indicatorSize.width + 6.0 } @@ -124,8 +128,8 @@ final class NetworkStatusTitleView: UIView { self.lockView.frame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 4.0, width: 2.0, height: 2.0) - if !self.activityIndicator.isHidden { - self.activityIndicator.frame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 6.0, y: titleFrame.minY + 1.0), size: indicatorSize) + if self.activityIndicator.layer.superlayer != nil { + self.activityIndicator.frame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 6.0, y: titleFrame.minY - 1.0), size: indicatorSize) } } @@ -151,4 +155,13 @@ final class NetworkStatusTitleView: UIView { @objc func buttonPressed() { self.toggleIsLocked?() } + + func makeTransitionMirrorNode() -> ASDisplayNode { + let view = NetworkStatusTitleView(theme: self.theme) + view.title = self.title + + return ASDisplayNode(viewBlock: { + return view + }, didLoad: nil) + } } diff --git a/TelegramUI/NotificationContainerControllerNode.swift b/TelegramUI/NotificationContainerControllerNode.swift index da259af174..4a93cbd872 100644 --- a/TelegramUI/NotificationContainerControllerNode.swift +++ b/TelegramUI/NotificationContainerControllerNode.swift @@ -20,9 +20,11 @@ final class NotificationContainerControllerNode: ASDisplayNode { private var timeoutTimer: SwiftSignalKit.Timer? override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return NotificationContainerControllerNodeView() - }, didLoad: nil) + }) self.backgroundColor = nil self.isOpaque = false diff --git a/TelegramUI/OverlayMediaControllerNode.swift b/TelegramUI/OverlayMediaControllerNode.swift index f591cca721..57c705aff6 100644 --- a/TelegramUI/OverlayMediaControllerNode.swift +++ b/TelegramUI/OverlayMediaControllerNode.swift @@ -34,9 +34,11 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega private var draggingStartPosition = CGPoint() override init() { - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return OverlayMediaControllerNodeView() - }, didLoad: nil) + }) (self.view as! OverlayMediaControllerNodeView).hitTestImpl = { [weak self] point, event in return self?.hitTest(point, with: event) @@ -321,7 +323,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega let nodeSize = draggingNode.preferredSizeForOverlayDisplay() let previousFrame = draggingNode.frame - let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: previousFrame.origin, velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) + let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) if shouldDismiss && draggingNode.isMinimizeable { draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index 45b276df53..8b7fdb39b8 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private final class PasscodeOptionsControllerArguments { let turnPasscodeOn: () -> Void @@ -213,7 +213,8 @@ func passcodeOptionsController(account: Account) -> ViewController { let arguments = PasscodeOptionsControllerArguments(turnPasscodeOn: { var dismissImpl: (() -> Void)? - let controller = TGPasscodeEntryController(style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let challenge = PostboxAccessChallengeData.numericalPassword(value: result, timeout: nil, attempts: nil) let _ = account.postbox.modify({ modifier -> Void in @@ -229,7 +230,7 @@ func passcodeOptionsController(account: Account) -> ViewController { dismissImpl?() } })! - let legacyController = LegacyController(legacyController: controller, presentation: LegacyControllerPresentation.modal(animateIn: true)) + legacyController.bind(controller: controller) legacyController.supportedOrientations = .portrait legacyController.statusBar.statusBarStyle = .White presentControllerImpl?(legacyController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -259,7 +260,8 @@ func passcodeOptionsController(account: Account) -> ViewController { presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, changePasscode: { var dismissImpl: (() -> Void)? - let controller = TGPasscodeEntryController(style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeSetupSimple, cancelEnabled: true, allowTouchId: false, attemptData: nil, completion: { result in if let result = result { let _ = account.postbox.modify({ modifier -> Void in var data = modifier.getAccessChallengeData() @@ -276,7 +278,7 @@ func passcodeOptionsController(account: Account) -> ViewController { dismissImpl?() } })! - let legacyController = LegacyController(legacyController: controller, presentation: LegacyControllerPresentation.modal(animateIn: true)) + legacyController.bind(controller: controller) legacyController.supportedOrientations = .portrait legacyController.statusBar.statusBarStyle = .White presentControllerImpl?(legacyController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -365,7 +367,8 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp)) } var dismissImpl: (() -> Void)? - let controller = TGPasscodeEntryController(style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeVerifySimple, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true)) + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: TGPasscodeEntryControllerModeVerifySimple, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in if value != nil { completion(false) } @@ -403,7 +406,7 @@ public func passcodeOptionsAccessController(account: Account, animateIn: Bool = modifier.setAccessChallengeData(data) }).start() } - let legacyController = LegacyController(legacyController: controller, presentation: LegacyControllerPresentation.modal(animateIn: animateIn)) + legacyController.bind(controller: controller) legacyController.supportedOrientations = .portrait legacyController.statusBar.statusBarStyle = .White dismissImpl = { [weak legacyController] in diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index 76bf48264f..3de988e277 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -32,6 +32,8 @@ final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { for media in message.media { if let file = media as? TelegramMediaFile { return file.resource + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { + return file.resource } } return nil @@ -47,6 +49,10 @@ final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { if let file = media as? TelegramMediaFile { if file.isMusic { return true + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { + if file.isMusic { + return true + } } } } @@ -78,6 +84,24 @@ final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { } } return nil + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let file = content.file { + for attribute in file.attributes { + switch attribute { + case let .Audio(isVoice, duration, title, performer, _): + if isVoice { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) + } else { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) + } + case let .Video(duration, _, flags): + if flags.contains(.instantRoundVideo) { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) + } + default: + break + } + } + return nil } } case .HoleEntry: @@ -131,14 +155,14 @@ func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> switch attribute { case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { - tagMask = .VoiceOrInstantVideo + tagMask = .voiceOrInstantVideo break inner } case let .Audio(isVoice, _, _, _, _): if isVoice { - tagMask = .VoiceOrInstantVideo + tagMask = .voiceOrInstantVideo } else { - tagMask = .Music + tagMask = .music } break inner default: diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 77086b3ce4..972051c164 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -44,11 +44,13 @@ public class PeerMediaCollectionController: ViewController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.interfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) - super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear)) - self.titleView = PeerMediaCollectionTitleView(mediaCollectionInterfaceState: self.interfaceState, toggle: { [weak self] in + self.title = self.presentationData.strings.SharedMedia_TitleAll + + /*self.titleView = PeerMediaCollectionTitleView(mediaCollectionInterfaceState: self.interfaceState, toggle: { [weak self] in self?.updateInterfaceState { $0.withToggledSelectingMode() } - }) + })*/ self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style @@ -206,7 +208,7 @@ public class PeerMediaCollectionController: ViewController { }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in - }) + }, automaticMediaDownloadSettings: .none) self.controllerInteraction = controllerInteraction @@ -325,6 +327,8 @@ public class PeerMediaCollectionController: ViewController { }, reportPeer: { }, dismissReportPeer: { }, deleteChat: { + }, beginCall: { + }, toggleMessageStickerStarred: { _ in }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 35a8bc2c8c..04e94f897f 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -8,17 +8,17 @@ import TelegramCore private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) -> ASDisplayNode { switch mode { case .photoOrVideo: - return ChatHistoryGridNode(account: account, peerId: peerId, messageId: messageId, tagMask: .PhotoOrVideo, controllerInteraction: controllerInteraction) + return ChatHistoryGridNode(account: account, peerId: peerId, messageId: messageId, tagMask: .photoOrVideo, controllerInteraction: controllerInteraction) case .file: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .File, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .file, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) node.preloadPages = true return node case .music: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .Music, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .music, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) node.preloadPages = true return node case .webpage: - let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .WebPage, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) + let node = ChatHistoryListNode(account: account, peerId: peerId, tagMask: .webPage, messageId: messageId, controllerInteraction: controllerInteraction, mode: .list) node.preloadPages = true return node } @@ -30,6 +30,8 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { private let controllerInteraction: ChatControllerInteraction private let interfaceInteraction: ChatPanelInterfaceInteraction + private let sectionsNode: PeerMediaCollectionSectionsNode + private var historyNodeImpl: ASDisplayNode var historyNode: ChatHistoryNode { return self.historyNodeImpl as! ChatHistoryNode @@ -61,17 +63,42 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } self.mediaCollectionInterfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.sectionsNode = PeerMediaCollectionSectionsNode(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.historyNodeImpl = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction) self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings) - super.init(viewBlock: { - return UITracingLayerView() - }, didLoad: nil) + super.init() + self.setViewBlock({ + return UITracingLayerView() + }) + + self.historyNodeImpl.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.addSubnode(self.historyNodeImpl) + self.addSubnode(self.sectionsNode) + + self.sectionsNode.indexUpdated = { [weak self] index in + if let strongSelf = self { + let mode: PeerMediaCollectionMode + switch index { + case 0: + mode = .photoOrVideo + case 1: + mode = .file + case 2: + mode = .webpage + case 3: + mode = .music + default: + mode = .photoOrVideo + } + strongSelf.requestUpdateMediaCollectionInterfaceState(true, { $0.withMode(mode) }) + } + } } deinit { @@ -84,6 +111,11 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight + let sectionsHeight = self.sectionsNode.updateLayout(width: layout.size.width, transition: transition) + transition.updateFrame(node: self.sectionsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: sectionsHeight))) + + insets.top += sectionsHeight + if let selectionState = self.mediaCollectionInterfaceState.selectionState { let interfaceState = self.chatPresentationInterfaceState.updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) @@ -189,6 +221,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if self.mediaCollectionInterfaceState.mode != mediaCollectionInterfaceState.mode { if let containerLayout = self.containerLayout, self.candidateHistoryNode == nil || self.candidateHistoryNode!.1 != mediaCollectionInterfaceState.mode { let node = historyNodeImplForMode(mediaCollectionInterfaceState.mode, account: self.account, peerId: self.peerId, messageId: nil, controllerInteraction: self.controllerInteraction) + node.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.candidateHistoryNode = (node, mediaCollectionInterfaceState.mode) var insets = containerLayout.0.insets(options: [.input]) @@ -209,9 +242,14 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { |> deliverOnMainQueue).start(next: { [weak self, weak node] _ in if let strongSelf = self, let strongNode = node, strongNode == strongSelf.candidateHistoryNode?.0 { strongSelf.candidateHistoryNode = nil - strongSelf.insertSubnode(strongNode, aboveSubnode: strongSelf.historyNodeImpl) - strongSelf.historyNodeImpl.removeFromSupernode() + strongSelf.insertSubnode(strongNode, belowSubnode: strongSelf.historyNodeImpl) + + let previousNode = strongSelf.historyNodeImpl strongSelf.historyNodeImpl = strongNode + + previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in + previousNode?.removeFromSupernode() + }) } })) } diff --git a/TelegramUI/PeerMediaCollectionSectionsNode.swift b/TelegramUI/PeerMediaCollectionSectionsNode.swift new file mode 100644 index 0000000000..5c0cf81374 --- /dev/null +++ b/TelegramUI/PeerMediaCollectionSectionsNode.swift @@ -0,0 +1,59 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore + +final class PeerMediaCollectionSectionsNode: ASDisplayNode { + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let segmentedControl: UISegmentedControl + private let separatorNode: ASDisplayNode + + var indexUpdated: ((Int) -> Void)? + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + self.segmentedControl = UISegmentedControl(items: [ + strings.SharedMedia_CategoryMedia, + strings.SharedMedia_CategoryDocs, + strings.SharedMedia_CategoryLinks, + strings.SharedMedia_CategoryOther + ]) + self.segmentedControl.selectedSegmentIndex = 0 + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.displaysAsynchronously = false + self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor + + super.init() + + self.addSubnode(self.separatorNode) + self.view.addSubview(self.segmentedControl) + + self.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + + self.segmentedControl.addTarget(self, action: #selector(indexChanged), for: .valueChanged) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let panelHeight: CGFloat = 44.0 + + let controlHeight: CGFloat = 29.0 + let sideInset: CGFloat = 8.0 + transition.animateView { + self.segmentedControl.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((panelHeight - controlHeight) / 2.0)), size: CGSize(width: width - sideInset * 2.0, height: controlHeight)) + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + + return panelHeight + } + + @objc func indexChanged() { + self.indexUpdated?(self.segmentedControl.selectedSegmentIndex) + } +} diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index cc7ade6080..f6a0a34c1d 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -31,9 +31,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode = ChatListNode(account: account, mode: .peers, theme: presentationData.theme, strings: presentationData.strings) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.addSubnode(self.chatListNode) self.presentationDataDisposable = (account.telegramApplicationContext.presentationData diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 20fbda3099..21b48d2e19 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1460,7 +1460,7 @@ func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaIma private func builtinWallpaperData() -> Signal { return Signal { subscriber in - if let image = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0") { + if let filePath = frameworkBundle.path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg"), let image = UIImage(contentsOfFile: filePath) { subscriber.putNext(image) } subscriber.putCompletion() diff --git a/TelegramUI/PictureInPictureVideoControlsNode.swift b/TelegramUI/PictureInPictureVideoControlsNode.swift index 3ca80d774e..a7d355a2b2 100644 --- a/TelegramUI/PictureInPictureVideoControlsNode.swift +++ b/TelegramUI/PictureInPictureVideoControlsNode.swift @@ -3,7 +3,7 @@ import Display import AsyncDisplayKit import SwiftSignalKit -import TelegramLegacyComponents +import LegacyComponents private let leaveImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureLeave")?.precomposed() private let pauseImage = UIImage(bundleImageName: "Media Gallery/PictureInPicturePause")?.precomposed() diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 9eeac5a576..a8f1965777 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -47,8 +47,8 @@ private func dictFromLocalization(_ value: Localization) -> [String: String] { return dict } -public func currentPresentationData(postbox: Postbox) -> Signal { - return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?) in +public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings), NoError> { + return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings) in let themeSettings: PresentationThemeSettings if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current @@ -63,8 +63,15 @@ public func currentPresentationData(postbox: Postbox) -> Signal map { (themeSettings, localizationSettings) -> PresentationData in + let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings + if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings) as? AutomaticMediaDownloadSettings { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings + } + + return (themeSettings, localizationSettings, automaticMediaDownloadSettings) + } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings) -> (PresentationData, AutomaticMediaDownloadSettings) in let themeValue: PresentationTheme switch themeSettings.theme { case let .builtin(reference): @@ -81,7 +88,7 @@ public func currentPresentationData(postbox: Postbox) -> Signal UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelSavedStickersIconImage.rawValue, { theme in + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/SavedStickersTabIcon"), color: theme.chat.inputMediaPanel.panelIconColor) { + 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)), size: image.size)) + } + }) + }) + } + static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in return generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in @@ -391,6 +402,23 @@ struct PresentationResourcesChat { }) } + static func chatHistoryMentionsButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatHistoryMentionsButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.historyNavigation.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0))) + context.setLineWidth(0.5) + context.setStrokeColor(theme.chat.historyNavigation.strokeColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/NavigateToMentions"), color: theme.chat.historyNavigation.foregroundColor), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }) + }) + } + static func chatHistoryNavigationButtonBadgeImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatHistoryNavigationButtonBadgeImage.rawValue, { theme in return generateStretchableFilledCircleImage(diameter: 18.0, color: theme.chat.historyNavigation.badgeBackgroundColor, backgroundColor: nil) @@ -477,4 +505,71 @@ struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/Calendar"), color: theme.chat.inputPanel.panelControlAccentColor) }) } + + /* + case chatTitlePanelSearchImage + case chatTitlePanelMuteImage + case chatTitlePanelUnmuteImage + case chatTitlePanelCallImage + */ + + static func chatTitlePanelInfoImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelInfoImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/InfoIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatTitlePanelSearchImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelSearchImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/SearchIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatTitlePanelMuteImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelMuteImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/RevealActionMuteIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatTitlePanelUnmuteImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelUnmuteImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/RevealActionMuteIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatTitlePanelCallImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelCallImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconCalls"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatTitlePanelReportImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitlePanelReportImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/ReportIcon"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatMessageAttachedContentButtonIncoming(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIncoming.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 8.0, color: nil, strokeColor: theme.chat.bubble.incomingAccentColor, strokeWidth: 1.0, backgroundColor: nil) + }) + } + + static func chatMessageAttachedContentHighlightedButtonIncoming(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIncoming.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 8.0, color: theme.chat.bubble.incomingAccentColor) + }) + } + + static func chatMessageAttachedContentButtonOutgoing(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonOutgoing.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 8.0, color: nil, strokeColor: theme.chat.bubble.outgoingAccentColor, strokeWidth: 1.0, backgroundColor: nil) + }) + } + + static func chatMessageAttachedContentHighlightedButtonOutgoing(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonOutgoing.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 8.0, color: theme.chat.bubble.outgoingAccentColor) + }) + } } diff --git a/TelegramUI/PresentationResourcesChatList.swift b/TelegramUI/PresentationResourcesChatList.swift index 6082dfa7e7..80e1fe9990 100644 --- a/TelegramUI/PresentationResourcesChatList.swift +++ b/TelegramUI/PresentationResourcesChatList.swift @@ -22,7 +22,7 @@ private func generateStatusCheckImage(theme: PresentationTheme, single: Bool) -> }) } -private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool) -> UIImage? { +private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool, icon: UIImage? = nil) -> UIImage? { return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) if active { @@ -31,6 +31,9 @@ private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool context.setFillColor(theme.chatList.unreadBadgeInactiveBackgroundColor.cgColor) } context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + if let icon = icon, let cgImage = icon.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)) + } })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) } @@ -82,4 +85,10 @@ struct PresentationResourcesChatList { return generateBadgeBackgroundImage(theme: theme, active: false) }) } + + static func badgeBackgroundMention(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListBadgeBackgroundMention.rawValue, { theme in + return generateBadgeBackgroundImage(theme: theme, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: .white)) + }) + } } diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index c9fe6786d6..c374b0e85b 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -13,14 +13,18 @@ private func generateShareButtonImage(theme: PresentationTheme) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.rootController.navigationBar.accentTextColor) } +func generateIndefiniteActivityIndicatorImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ") + }) +} + struct PresentationResourcesRootController { static func navigationIndefiniteActivityImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.rootNavigationIndefiniteActivity.rawValue, { theme in - return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) - let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ") - }) + generateIndefiniteActivityIndicatorImage(color: theme.rootController.navigationBar.accentTextColor) }) } diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index 08e642bf21..3cda7aa2bc 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -98,6 +98,7 @@ public final class PresentationStrings { public let languageCode: String public let Channel_BanUser_Title: String + public let Notification_SecretChatMessageScreenshotSelf: String public let Preview_SaveGif: String public let EnterPasscode_EnterNewPasscodeNew: String public let Privacy_Calls_WhoCanCallMe: String @@ -201,6 +202,7 @@ public final class PresentationStrings { public let TwoStepAuth_SetupEmail: String public let Login_ResetAccountProtected_Reset: String public let SocksProxySetup_Hostname: String + public let Channel_AdminLog_CanEditMessages: String private let _MESSAGE_CONTACT: String private let _MESSAGE_CONTACT_r: [(Int, NSRange)] public func MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -403,6 +405,7 @@ public final class PresentationStrings { } public let Channel_AdminLogFilter_EventsAll: String public let Call_ConnectionErrorTitle: String + public let Settings_ApplyProxyAlertEnable: String public let Settings_ChatSettings: String public let Group_About_Help: String private let _CHANNEL_MESSAGE_NOTEXT: String @@ -459,6 +462,7 @@ public final class PresentationStrings { public let PhotoEditor_Skip: String public let AuthSessions_TerminateOtherSessionsHelp: String public let Call_AudioRouteHeadphones: String + public let SocksProxySetup_UseForCalls: String public let Contacts_InviteFriends: String public let Channel_BanUser_PermissionSendMessages: String public let Notifications_InAppNotificationsVibrate: String @@ -628,6 +632,7 @@ public final class PresentationStrings { public let DialogList_NoMessagesTitle: String public let AccessDenied_Contacts: String public let Your_cards_security_code_is_invalid: String + public let Contacts_InviteSearchLabel: String public let Tour_StartButton: String public let CheckoutInfo_Title: String private let _Channel_AdminLog_MessageRestrictedNameUsername: String @@ -647,6 +652,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_DialogList_EncryptedChatStartedIncoming, self._DialogList_EncryptedChatStartedIncoming_r, [_0]) } public let Calls_AddTab: String + public let PhotoEditor_TiltShift: String public let ChannelMembers_WhoCanAddMembers_Admins: String public let Tour_Text5: String public let Watch_Stickers_RecentPlaceholder: String @@ -853,6 +859,11 @@ public final class PresentationStrings { public let Privacy_Calls_NeverAllow_Placeholder: String public let Settings_Support: String public let Notification_GroupInviterSelf: String + private let _SecretImage_NotViewedYet: String + private let _SecretImage_NotViewedYet_r: [(Int, NSRange)] + public func SecretImage_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SecretImage_NotViewedYet, self._SecretImage_NotViewedYet_r, [_0]) + } public let MaskStickerSettings_Title: String public let Watch_Suggestion_ThankYou: String public let TwoStepAuth_SetPassword: String @@ -860,6 +871,7 @@ public final class PresentationStrings { public let GroupInfo_InviteLink_ShareLink: String public let ChannelMembers_AllMembersMayInviteOnHelp: String public let Common_Cancel: String + public let UserInfo_About_Placeholder: String public let Preview_LoadingImages: String public let ChangePhoneNumberCode_RequestingACall: String public let PrivacyLastSeenSettings_NeverShareWith_Title: String @@ -880,6 +892,7 @@ public final class PresentationStrings { public let ShareMenu_CopyShareLink: String public let Channel_Setup_TypePrivateHelp: String public let PhotoEditor_GrainTool: String + public let Conversation_SearchByName_Placeholder: String public let Watch_Suggestion_TalkLater: String public let TwoStepAuth_ChangeEmail: String private let _ENCRYPTION_ACCEPT: String @@ -957,6 +970,11 @@ public final class PresentationStrings { public let DialogList_Create: String public let Tour_Title4: String public let Call_StatusEnded: String + private let _Channel_Management_RestrictedBy: String + private let _Channel_Management_RestrictedBy_r: [(Int, NSRange)] + public func Channel_Management_RestrictedBy(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_Management_RestrictedBy, self._Channel_Management_RestrictedBy_r, [_0]) + } public let Conversation_UnpinMessageAlert: String private let _Conversation_MessageDialogRetryAll: String private let _Conversation_MessageDialogRetryAll_r: [(Int, NSRange)] @@ -1084,6 +1102,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Time_MonthOfYear_m7, self._Time_MonthOfYear_m7_r, [_0]) } public let PhotoEditor_QualityLow: String + public let State_ConnectingToProxyInfo: String public let Paint_Outlined: String public let Checkout_PasswordEntry_Title: String public let Common_Done: String @@ -1212,6 +1231,7 @@ public final class PresentationStrings { public let Channel_Members_AddMembers: String public let Tour_Title2: String public let Login_TermsOfServiceHeader: String + public let Channel_AdminLog_BanSendGifs: String public let AuthSessions_OtherSessions: String public let Watch_UserInfo_Title: String public let InstantPage_FeedbackButton: String @@ -1282,6 +1302,11 @@ public final class PresentationStrings { public let Weekday_ShortThursday: String public let UserInfo_ShareContact: String public let LoginPassword_InvalidPasswordError: String + private let _MESSAGE_PHOTO_SECRET: String + private let _MESSAGE_PHOTO_SECRET_r: [(Int, NSRange)] + public func MESSAGE_PHOTO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_PHOTO_SECRET, self._MESSAGE_PHOTO_SECRET_r, [_1]) + } public let Login_PhoneAndCountryHelp: String public let CheckoutInfo_ReceiverInfoName: String private let _LastSeen_TodayAt: String @@ -1302,6 +1327,11 @@ public final class PresentationStrings { public let Conversation_Play: String public let Settings_PrivacySettings: String public let Conversation_SilentBroadcastTooltipOn: String + private let _SecretVideo_NotViewedYet: String + private let _SecretVideo_NotViewedYet_r: [(Int, NSRange)] + public func SecretVideo_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SecretVideo_NotViewedYet, self._SecretVideo_NotViewedYet_r, [_0]) + } private let _CHAT_MESSAGE_GEO: String private let _CHAT_MESSAGE_GEO_r: [(Int, NSRange)] public func CHAT_MESSAGE_GEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1364,6 +1394,7 @@ public final class PresentationStrings { public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_AuthSessions_AppUnofficial, self._AuthSessions_AppUnofficial_r, [_0]) } + public let Conversation_ContextMenuBan: String public let Channel_EditAdmin_PermissionsHeader: String private let _DialogList_SingleUploadingVideoSuffix: String private let _DialogList_SingleUploadingVideoSuffix_r: [(Int, NSRange)] @@ -1391,6 +1422,7 @@ public final class PresentationStrings { public let ShareFileTip_CloseTip: String public let KeyCommand_Find: String public let Preview_VideoNotYetDownloaded: String + public let SecretVideo_Title: String public let Checkout_NewCard_PostcodeTitle: String private let _Channel_AdminLog_MessageRestricted: String private let _Channel_AdminLog_MessageRestricted_r: [(Int, NSRange)] @@ -1527,6 +1559,7 @@ public final class PresentationStrings { } public let Tour_Text2: String public let Preview_ViewStickerPack: String + public let Call_StatusNoAnswer: String public let Conversation_MessageDialogDelete: String public let Calls_Clear: String public let Username_Placeholder: String @@ -1558,6 +1591,7 @@ public final class PresentationStrings { public let Conversation_InfoPrivate: String public let PasscodeSettings_Help: String public let Conversation_EditingMessagePanelTitle: String + public let Settings_AboutEmpty: String private let _NetworkUsageSettings_CellularUsageSince: String private let _NetworkUsageSettings_CellularUsageSince_r: [(Int, NSRange)] public func NetworkUsageSettings_CellularUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -1761,6 +1795,7 @@ public final class PresentationStrings { public let Conversation_ReportSpam: String public let Camera_FlashAuto: String public let Call_ConnectionErrorMessage: String + public let Stickers_FrequentlyUsed: String public let Compose_NewChannel_AddMember: String public let Watch_State_Updating: String public let LastSeen_ALongTimeAgo: String @@ -1792,6 +1827,11 @@ public final class PresentationStrings { public func Contacts_AddPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Contacts_AddPhoneNumber, self._Contacts_AddPhoneNumber_r, [_0]) } + private let _MESSAGE_SCREENSHOT: String + private let _MESSAGE_SCREENSHOT_r: [(Int, NSRange)] + public func MESSAGE_SCREENSHOT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_SCREENSHOT, self._MESSAGE_SCREENSHOT_r, [_1]) + } public let DialogList_EncryptionProcessing: String public let Conversation_ApplyLocalization: String public let Conversation_DeleteManyMessages: String @@ -1858,6 +1898,7 @@ public final class PresentationStrings { public func Time_MonthOfYear_m4(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Time_MonthOfYear_m4, self._Time_MonthOfYear_m4_r, [_0]) } + public let SecretImage_Title: String public let Preview_ForwardViaTelegram: String public let Notifications_InAppNotificationsSounds: String public let Call_StatusRequesting: String @@ -1923,6 +1964,7 @@ public final class PresentationStrings { public func PINNED_GAME(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_GAME, self._PINNED_GAME_r, [_1]) } + public let Invite_LargeRecipientsCountWarning: String public let GroupInfo_BroadcastListNamePlaceholder: String public let Conversation_ShareBotContactConfirmation: String public let GroupInfo_ActionBan: String @@ -2067,6 +2109,7 @@ public final class PresentationStrings { public let Month_ShortJuly: String public let Watch_MessageView_ViewOnPhone: String public let CheckoutInfo_ShippingInfoAddress1Placeholder: String + public let Stickers_Favorited: String public let CallSettings_Never: String public let DialogList_SelectContacts: String public let Conversation_DownloadProgressMegabytes: String @@ -2136,11 +2179,17 @@ public final class PresentationStrings { } public let CallSettings_RecentCalls: String public let Conversation_Megabytes: String + public let Conversation_SearchByName_Prefix: String public let TwoStepAuth_FloodError: String public let Login_InvalidCountryCode: String public let Paint_Stickers: String public let Privacy_Calls_AlwaysAllow_Title: String public let Username_InvalidTooShort: String + private let _Settings_ApplyProxyAlert: String + private let _Settings_ApplyProxyAlert_r: [(Int, NSRange)] + public func Settings_ApplyProxyAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Settings_ApplyProxyAlert, self._Settings_ApplyProxyAlert_r, [_1, _2]) + } public let Weekday_ShortFriday: String public let Conversation_ClearAll: String public let MediaPicker_Moments: String @@ -2167,6 +2216,7 @@ public final class PresentationStrings { } public let Conversation_EncryptionCanceled: String public let AccessDenied_SaveMedia: String + public let InviteText_URL: String private let _Channel_AdminLog_MessageInvitedNameUsername: String private let _Channel_AdminLog_MessageInvitedNameUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageInvitedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -2529,6 +2579,7 @@ public final class PresentationStrings { public func MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_MESSAGE_INVOICE, self._MESSAGE_INVOICE_r, [_1, _2]) } + public let Localization_LanguageCustom: String private let _Channel_AdminLog_MessageRemovedChannelUsername: String private let _Channel_AdminLog_MessageRemovedChannelUsername_r: [(Int, NSRange)] public func Channel_AdminLog_MessageRemovedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2547,6 +2598,7 @@ public final class PresentationStrings { public let ChatSettings_ConnectionType_UseSocks5: String public let MediaPicker_MomentsDateRangeYearFormat: String public let Cache_ClearNone: String + public let SecretTimer_VideoDescription: String public let Login_InvalidCodeError: String public let Contacts_contacts: String public let Channel_BanList_BlockedTitle: String @@ -2694,6 +2746,11 @@ public final class PresentationStrings { public func PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PINNED_GIF, self._PINNED_GIF_r, [_1]) } + private let _InviteText_SingleContact: String + private let _InviteText_SingleContact_r: [(Int, NSRange)] + public func InviteText_SingleContact(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_InviteText_SingleContact, self._InviteText_SingleContact_r, [_0]) + } public let Channel_EditAdmin_CannotEdit: String public let Profile_PhonebookAccessDisabled: String public let LoginPassword_PasswordHelp: String @@ -2707,6 +2764,7 @@ public final class PresentationStrings { public let Notifications_GroupNotificationsAlert: String public let Paint_Masks: String public let StickerPack_ErrorNotFound: String + public let SecretTimer_ImageDescription: String private let _PINNED_CONTACT: String private let _PINNED_CONTACT_r: [(Int, NSRange)] public func PINNED_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { @@ -2795,6 +2853,11 @@ public final class PresentationStrings { public let Watch_Location_Current: String public let Map_MapTitle: String public let Checkout_NewCard_SaveInfoHelp: String + private let _Settings_ApplyProxyAlertCredentials: String + private let _Settings_ApplyProxyAlertCredentials_r: [(Int, NSRange)] + public func Settings_ApplyProxyAlertCredentials(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Settings_ApplyProxyAlertCredentials, self._Settings_ApplyProxyAlertCredentials_r, [_1, _2, _3, _4]) + } public let MediaPicker_CameraRoll: String private let _TwoStepAuth_RecoverySent: String private let _TwoStepAuth_RecoverySent_r: [(Int, NSRange)] @@ -2872,6 +2935,11 @@ public final class PresentationStrings { public let Login_InfoLastNamePlaceholder: String public let Contacts_InvitationText: String public let Channel_Members_AddMembersHelp: String + private let _MESSAGE_VIDEO_SECRET: String + private let _MESSAGE_VIDEO_SECRET_r: [(Int, NSRange)] + public func MESSAGE_VIDEO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_VIDEO_SECRET, self._MESSAGE_VIDEO_SECRET_r, [_1]) + } public let ReportPeer_Report: String public let Channel_EditMessageErrorGeneric: String public let LoginPassword_FloodError: String @@ -3012,6 +3080,7 @@ public final class PresentationStrings { } public let MessageTimer_Title: String public let Watch_Compose_Send: String + public let SocksProxySetup_UseForCallsHelp: String public let Preview_CopyAddress: String public let Settings_BlockedUsers: String public let Month_ShortAugust: String @@ -3596,6 +3665,28 @@ public final class PresentationStrings { return String(format: self._SharedMedia_Generic_other, "\(value)") } } + private let _InviteText_ContactsCount_zero: String + private let _InviteText_ContactsCount_one: String + private let _InviteText_ContactsCount_two: String + private let _InviteText_ContactsCount_few: String + private let _InviteText_ContactsCount_many: String + private let _InviteText_ContactsCount_other: String + public func InviteText_ContactsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._InviteText_ContactsCount_zero, "\(value)") + case .one: + return String(format: self._InviteText_ContactsCount_one, "\(value)") + case .two: + return String(format: self._InviteText_ContactsCount_two, "\(value)") + case .few: + return String(format: self._InviteText_ContactsCount_few, "\(value)") + case .many: + return String(format: self._InviteText_ContactsCount_many, "\(value)") + case .other: + return String(format: self._InviteText_ContactsCount_other, "\(value)") + } + } private let _Conversation_StatusMembers_zero: String private let _Conversation_StatusMembers_one: String private let _Conversation_StatusMembers_two: String @@ -4652,6 +4743,28 @@ public final class PresentationStrings { return String(format: self._AttachmentMenu_SendItem_other, "\(value)") } } + private let _Contacts_ImportersCount_zero: String + private let _Contacts_ImportersCount_one: String + private let _Contacts_ImportersCount_two: String + private let _Contacts_ImportersCount_few: String + private let _Contacts_ImportersCount_many: String + private let _Contacts_ImportersCount_other: String + public func Contacts_ImportersCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Contacts_ImportersCount_zero, "\(value)") + case .one: + return String(format: self._Contacts_ImportersCount_one, "\(value)") + case .two: + return String(format: self._Contacts_ImportersCount_two, "\(value)") + case .few: + return String(format: self._Contacts_ImportersCount_few, "\(value)") + case .many: + return String(format: self._Contacts_ImportersCount_many, "\(value)") + case .other: + return String(format: self._Contacts_ImportersCount_other, "\(value)") + } + } private let _Watch_UserInfo_Mute_zero: String private let _Watch_UserInfo_Mute_one: String private let _Watch_UserInfo_Mute_two: String @@ -4888,6 +5001,7 @@ public final class PresentationStrings { } self.lc = lc self.Channel_BanUser_Title = getValue(dict, "Channel.BanUser.Title") + self.Notification_SecretChatMessageScreenshotSelf = getValue(dict, "Notification.SecretChatMessageScreenshotSelf") self.Preview_SaveGif = getValue(dict, "Preview.SaveGif") self.EnterPasscode_EnterNewPasscodeNew = getValue(dict, "EnterPasscode.EnterNewPasscodeNew") self.Privacy_Calls_WhoCanCallMe = getValue(dict, "Privacy.Calls.WhoCanCallMe") @@ -4955,6 +5069,7 @@ public final class PresentationStrings { self.TwoStepAuth_SetupEmail = getValue(dict, "TwoStepAuth.SetupEmail") self.Login_ResetAccountProtected_Reset = getValue(dict, "Login.ResetAccountProtected.Reset") self.SocksProxySetup_Hostname = getValue(dict, "SocksProxySetup.Hostname") + self.Channel_AdminLog_CanEditMessages = getValue(dict, "Channel.AdminLog.CanEditMessages") self._MESSAGE_CONTACT = getValue(dict, "MESSAGE_CONTACT") self._MESSAGE_CONTACT_r = extractArgumentRanges(self._MESSAGE_CONTACT) self._Group_Management_ErrorNotMember = getValue(dict, "Group.Management.ErrorNotMember") @@ -5091,6 +5206,7 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_DOC_r = extractArgumentRanges(self._CHANNEL_MESSAGE_DOC) self.Channel_AdminLogFilter_EventsAll = getValue(dict, "Channel.AdminLogFilter.EventsAll") self.Call_ConnectionErrorTitle = getValue(dict, "Call.ConnectionErrorTitle") + self.Settings_ApplyProxyAlertEnable = getValue(dict, "Settings.ApplyProxyAlertEnable") self.Settings_ChatSettings = getValue(dict, "Settings.ChatSettings") self.Group_About_Help = getValue(dict, "Group.About.Help") self._CHANNEL_MESSAGE_NOTEXT = getValue(dict, "CHANNEL_MESSAGE_NOTEXT") @@ -5132,6 +5248,7 @@ public final class PresentationStrings { self.PhotoEditor_Skip = getValue(dict, "PhotoEditor.Skip") self.AuthSessions_TerminateOtherSessionsHelp = getValue(dict, "AuthSessions.TerminateOtherSessionsHelp") self.Call_AudioRouteHeadphones = getValue(dict, "Call.AudioRouteHeadphones") + self.SocksProxySetup_UseForCalls = getValue(dict, "SocksProxySetup.UseForCalls") self.Contacts_InviteFriends = getValue(dict, "Contacts.InviteFriends") self.Channel_BanUser_PermissionSendMessages = getValue(dict, "Channel.BanUser.PermissionSendMessages") self.Notifications_InAppNotificationsVibrate = getValue(dict, "Notifications.InAppNotificationsVibrate") @@ -5253,6 +5370,7 @@ public final class PresentationStrings { self.DialogList_NoMessagesTitle = getValue(dict, "DialogList.NoMessagesTitle") self.AccessDenied_Contacts = getValue(dict, "AccessDenied.Contacts") self.Your_cards_security_code_is_invalid = getValue(dict, "Your_cards_security_code_is_invalid") + self.Contacts_InviteSearchLabel = getValue(dict, "Contacts.InviteSearchLabel") self.Tour_StartButton = getValue(dict, "Tour.StartButton") self.CheckoutInfo_Title = getValue(dict, "CheckoutInfo.Title") self._Channel_AdminLog_MessageRestrictedNameUsername = getValue(dict, "Channel.AdminLog.MessageRestrictedNameUsername") @@ -5266,6 +5384,7 @@ public final class PresentationStrings { self._DialogList_EncryptedChatStartedIncoming = getValue(dict, "DialogList.EncryptedChatStartedIncoming") self._DialogList_EncryptedChatStartedIncoming_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedIncoming) self.Calls_AddTab = getValue(dict, "Calls.AddTab") + self.PhotoEditor_TiltShift = getValue(dict, "PhotoEditor.TiltShift") self.ChannelMembers_WhoCanAddMembers_Admins = getValue(dict, "ChannelMembers.WhoCanAddMembers.Admins") self.Tour_Text5 = getValue(dict, "Tour.Text5") self.Watch_Stickers_RecentPlaceholder = getValue(dict, "Watch.Stickers.RecentPlaceholder") @@ -5421,6 +5540,8 @@ public final class PresentationStrings { self.Privacy_Calls_NeverAllow_Placeholder = getValue(dict, "Privacy.Calls.NeverAllow.Placeholder") self.Settings_Support = getValue(dict, "Settings.Support") self.Notification_GroupInviterSelf = getValue(dict, "Notification.GroupInviterSelf") + self._SecretImage_NotViewedYet = getValue(dict, "SecretImage.NotViewedYet") + self._SecretImage_NotViewedYet_r = extractArgumentRanges(self._SecretImage_NotViewedYet) self.MaskStickerSettings_Title = getValue(dict, "MaskStickerSettings.Title") self.Watch_Suggestion_ThankYou = getValue(dict, "Watch.Suggestion.ThankYou") self.TwoStepAuth_SetPassword = getValue(dict, "TwoStepAuth.SetPassword") @@ -5428,6 +5549,7 @@ public final class PresentationStrings { self.GroupInfo_InviteLink_ShareLink = getValue(dict, "GroupInfo.InviteLink.ShareLink") self.ChannelMembers_AllMembersMayInviteOnHelp = getValue(dict, "ChannelMembers.AllMembersMayInviteOnHelp") self.Common_Cancel = getValue(dict, "Common.Cancel") + self.UserInfo_About_Placeholder = getValue(dict, "UserInfo.About.Placeholder") self.Preview_LoadingImages = getValue(dict, "Preview.LoadingImages") self.ChangePhoneNumberCode_RequestingACall = getValue(dict, "ChangePhoneNumberCode.RequestingACall") self.PrivacyLastSeenSettings_NeverShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Title") @@ -5445,6 +5567,7 @@ public final class PresentationStrings { self.ShareMenu_CopyShareLink = getValue(dict, "ShareMenu.CopyShareLink") self.Channel_Setup_TypePrivateHelp = getValue(dict, "Channel.Setup.TypePrivateHelp") self.PhotoEditor_GrainTool = getValue(dict, "PhotoEditor.GrainTool") + self.Conversation_SearchByName_Placeholder = getValue(dict, "Conversation.SearchByName.Placeholder") self.Watch_Suggestion_TalkLater = getValue(dict, "Watch.Suggestion.TalkLater") self.TwoStepAuth_ChangeEmail = getValue(dict, "TwoStepAuth.ChangeEmail") self._ENCRYPTION_ACCEPT = getValue(dict, "ENCRYPTION_ACCEPT") @@ -5501,6 +5624,8 @@ public final class PresentationStrings { self.DialogList_Create = getValue(dict, "DialogList.Create") self.Tour_Title4 = getValue(dict, "Tour.Title4") self.Call_StatusEnded = getValue(dict, "Call.StatusEnded") + self._Channel_Management_RestrictedBy = getValue(dict, "Channel.Management.RestrictedBy") + self._Channel_Management_RestrictedBy_r = extractArgumentRanges(self._Channel_Management_RestrictedBy) self.Conversation_UnpinMessageAlert = getValue(dict, "Conversation.UnpinMessageAlert") self._Conversation_MessageDialogRetryAll = getValue(dict, "Conversation.MessageDialogRetryAll") self._Conversation_MessageDialogRetryAll_r = extractArgumentRanges(self._Conversation_MessageDialogRetryAll) @@ -5595,6 +5720,7 @@ public final class PresentationStrings { self._Time_MonthOfYear_m7 = getValue(dict, "Time.MonthOfYear_m7") self._Time_MonthOfYear_m7_r = extractArgumentRanges(self._Time_MonthOfYear_m7) self.PhotoEditor_QualityLow = getValue(dict, "PhotoEditor.QualityLow") + self.State_ConnectingToProxyInfo = getValue(dict, "State.ConnectingToProxyInfo") self.Paint_Outlined = getValue(dict, "Paint.Outlined") self.Checkout_PasswordEntry_Title = getValue(dict, "Checkout.PasswordEntry.Title") self.Common_Done = getValue(dict, "Common.Done") @@ -5684,6 +5810,7 @@ public final class PresentationStrings { self.Channel_Members_AddMembers = getValue(dict, "Channel.Members.AddMembers") self.Tour_Title2 = getValue(dict, "Tour.Title2") self.Login_TermsOfServiceHeader = getValue(dict, "Login.TermsOfServiceHeader") + self.Channel_AdminLog_BanSendGifs = getValue(dict, "Channel.AdminLog.BanSendGifs") self.AuthSessions_OtherSessions = getValue(dict, "AuthSessions.OtherSessions") self.Watch_UserInfo_Title = getValue(dict, "Watch.UserInfo.Title") self.InstantPage_FeedbackButton = getValue(dict, "InstantPage.FeedbackButton") @@ -5742,6 +5869,8 @@ public final class PresentationStrings { self.Weekday_ShortThursday = getValue(dict, "Weekday.ShortThursday") self.UserInfo_ShareContact = getValue(dict, "UserInfo.ShareContact") self.LoginPassword_InvalidPasswordError = getValue(dict, "LoginPassword.InvalidPasswordError") + self._MESSAGE_PHOTO_SECRET = getValue(dict, "MESSAGE_PHOTO_SECRET") + self._MESSAGE_PHOTO_SECRET_r = extractArgumentRanges(self._MESSAGE_PHOTO_SECRET) self.Login_PhoneAndCountryHelp = getValue(dict, "Login.PhoneAndCountryHelp") self.CheckoutInfo_ReceiverInfoName = getValue(dict, "CheckoutInfo.ReceiverInfoName") self._LastSeen_TodayAt = getValue(dict, "LastSeen.TodayAt") @@ -5756,6 +5885,8 @@ public final class PresentationStrings { self.Conversation_Play = getValue(dict, "Conversation.Play") self.Settings_PrivacySettings = getValue(dict, "Settings.PrivacySettings") self.Conversation_SilentBroadcastTooltipOn = getValue(dict, "Conversation.SilentBroadcastTooltipOn") + self._SecretVideo_NotViewedYet = getValue(dict, "SecretVideo.NotViewedYet") + self._SecretVideo_NotViewedYet_r = extractArgumentRanges(self._SecretVideo_NotViewedYet) self._CHAT_MESSAGE_GEO = getValue(dict, "CHAT_MESSAGE_GEO") self._CHAT_MESSAGE_GEO_r = extractArgumentRanges(self._CHAT_MESSAGE_GEO) self.DialogList_SearchLabel = getValue(dict, "DialogList.SearchLabel") @@ -5794,6 +5925,7 @@ public final class PresentationStrings { self.Channel_AdminLogFilter_EventsAdmins = getValue(dict, "Channel.AdminLogFilter.EventsAdmins") self._AuthSessions_AppUnofficial = getValue(dict, "AuthSessions.AppUnofficial") self._AuthSessions_AppUnofficial_r = extractArgumentRanges(self._AuthSessions_AppUnofficial) + self.Conversation_ContextMenuBan = getValue(dict, "Conversation.ContextMenuBan") self.Channel_EditAdmin_PermissionsHeader = getValue(dict, "Channel.EditAdmin.PermissionsHeader") self._DialogList_SingleUploadingVideoSuffix = getValue(dict, "DialogList.SingleUploadingVideoSuffix") self._DialogList_SingleUploadingVideoSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingVideoSuffix) @@ -5809,6 +5941,7 @@ public final class PresentationStrings { self.ShareFileTip_CloseTip = getValue(dict, "ShareFileTip.CloseTip") self.KeyCommand_Find = getValue(dict, "KeyCommand.Find") self.Preview_VideoNotYetDownloaded = getValue(dict, "Preview.VideoNotYetDownloaded") + self.SecretVideo_Title = getValue(dict, "SecretVideo.Title") self.Checkout_NewCard_PostcodeTitle = getValue(dict, "Checkout.NewCard.PostcodeTitle") self._Channel_AdminLog_MessageRestricted = getValue(dict, "Channel.AdminLog.MessageRestricted") self._Channel_AdminLog_MessageRestricted_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestricted) @@ -5900,6 +6033,7 @@ public final class PresentationStrings { self._Call_ParticipantVersionOutdatedError_r = extractArgumentRanges(self._Call_ParticipantVersionOutdatedError) self.Tour_Text2 = getValue(dict, "Tour.Text2") self.Preview_ViewStickerPack = getValue(dict, "Preview.ViewStickerPack") + self.Call_StatusNoAnswer = getValue(dict, "Call.StatusNoAnswer") self.Conversation_MessageDialogDelete = getValue(dict, "Conversation.MessageDialogDelete") self.Calls_Clear = getValue(dict, "Calls.Clear") self.Username_Placeholder = getValue(dict, "Username.Placeholder") @@ -5922,6 +6056,7 @@ public final class PresentationStrings { self.Conversation_InfoPrivate = getValue(dict, "Conversation.InfoPrivate") self.PasscodeSettings_Help = getValue(dict, "PasscodeSettings.Help") self.Conversation_EditingMessagePanelTitle = getValue(dict, "Conversation.EditingMessagePanelTitle") + self.Settings_AboutEmpty = getValue(dict, "Settings.AboutEmpty") self._NetworkUsageSettings_CellularUsageSince = getValue(dict, "NetworkUsageSettings.CellularUsageSince") self._NetworkUsageSettings_CellularUsageSince_r = extractArgumentRanges(self._NetworkUsageSettings_CellularUsageSince) self.GroupInfo_ConvertToSupergroup = getValue(dict, "GroupInfo.ConvertToSupergroup") @@ -6059,6 +6194,7 @@ public final class PresentationStrings { self.Conversation_ReportSpam = getValue(dict, "Conversation.ReportSpam") self.Camera_FlashAuto = getValue(dict, "Camera.FlashAuto") self.Call_ConnectionErrorMessage = getValue(dict, "Call.ConnectionErrorMessage") + self.Stickers_FrequentlyUsed = getValue(dict, "Stickers.FrequentlyUsed") self.Compose_NewChannel_AddMember = getValue(dict, "Compose.NewChannel.AddMember") self.Watch_State_Updating = getValue(dict, "Watch.State.Updating") self.LastSeen_ALongTimeAgo = getValue(dict, "LastSeen.ALongTimeAgo") @@ -6081,6 +6217,8 @@ public final class PresentationStrings { self.Channel_EditAdmin_PermissionPostMessages = getValue(dict, "Channel.EditAdmin.PermissionPostMessages") self._Contacts_AddPhoneNumber = getValue(dict, "Contacts.AddPhoneNumber") self._Contacts_AddPhoneNumber_r = extractArgumentRanges(self._Contacts_AddPhoneNumber) + self._MESSAGE_SCREENSHOT = getValue(dict, "MESSAGE_SCREENSHOT") + self._MESSAGE_SCREENSHOT_r = extractArgumentRanges(self._MESSAGE_SCREENSHOT) self.DialogList_EncryptionProcessing = getValue(dict, "DialogList.EncryptionProcessing") self.Conversation_ApplyLocalization = getValue(dict, "Conversation.ApplyLocalization") self.Conversation_DeleteManyMessages = getValue(dict, "Conversation.DeleteManyMessages") @@ -6126,6 +6264,7 @@ public final class PresentationStrings { self._PrivacySettings_LastSeenNobodyPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenNobodyPlus) self._Time_MonthOfYear_m4 = getValue(dict, "Time.MonthOfYear_m4") self._Time_MonthOfYear_m4_r = extractArgumentRanges(self._Time_MonthOfYear_m4) + self.SecretImage_Title = getValue(dict, "SecretImage.Title") self.Preview_ForwardViaTelegram = getValue(dict, "Preview.ForwardViaTelegram") self.Notifications_InAppNotificationsSounds = getValue(dict, "Notifications.InAppNotificationsSounds") self.Call_StatusRequesting = getValue(dict, "Call.StatusRequesting") @@ -6164,6 +6303,7 @@ public final class PresentationStrings { self._Channel_Username_UsernameIsAvailable_r = extractArgumentRanges(self._Channel_Username_UsernameIsAvailable) self._PINNED_GAME = getValue(dict, "PINNED_GAME") self._PINNED_GAME_r = extractArgumentRanges(self._PINNED_GAME) + self.Invite_LargeRecipientsCountWarning = getValue(dict, "Invite.LargeRecipientsCountWarning") self.GroupInfo_BroadcastListNamePlaceholder = getValue(dict, "GroupInfo.BroadcastListNamePlaceholder") self.Conversation_ShareBotContactConfirmation = getValue(dict, "Conversation.ShareBotContactConfirmation") self.GroupInfo_ActionBan = getValue(dict, "GroupInfo.ActionBan") @@ -6263,6 +6403,7 @@ public final class PresentationStrings { self.Month_ShortJuly = getValue(dict, "Month.ShortJuly") self.Watch_MessageView_ViewOnPhone = getValue(dict, "Watch.MessageView.ViewOnPhone") self.CheckoutInfo_ShippingInfoAddress1Placeholder = getValue(dict, "CheckoutInfo.ShippingInfoAddress1Placeholder") + self.Stickers_Favorited = getValue(dict, "Stickers.Favorited") self.CallSettings_Never = getValue(dict, "CallSettings.Never") self.DialogList_SelectContacts = getValue(dict, "DialogList.SelectContacts") self.Conversation_DownloadProgressMegabytes = getValue(dict, "Conversation.DownloadProgressMegabytes") @@ -6311,11 +6452,14 @@ public final class PresentationStrings { self._Compatibility_SecretMediaVersionTooLow_r = extractArgumentRanges(self._Compatibility_SecretMediaVersionTooLow) self.CallSettings_RecentCalls = getValue(dict, "CallSettings.RecentCalls") self.Conversation_Megabytes = getValue(dict, "Conversation.Megabytes") + self.Conversation_SearchByName_Prefix = getValue(dict, "Conversation.SearchByName.Prefix") self.TwoStepAuth_FloodError = getValue(dict, "TwoStepAuth.FloodError") self.Login_InvalidCountryCode = getValue(dict, "Login.InvalidCountryCode") self.Paint_Stickers = getValue(dict, "Paint.Stickers") self.Privacy_Calls_AlwaysAllow_Title = getValue(dict, "Privacy.Calls.AlwaysAllow.Title") self.Username_InvalidTooShort = getValue(dict, "Username.InvalidTooShort") + self._Settings_ApplyProxyAlert = getValue(dict, "Settings.ApplyProxyAlert") + self._Settings_ApplyProxyAlert_r = extractArgumentRanges(self._Settings_ApplyProxyAlert) self.Weekday_ShortFriday = getValue(dict, "Weekday.ShortFriday") self.Conversation_ClearAll = getValue(dict, "Conversation.ClearAll") self.MediaPicker_Moments = getValue(dict, "MediaPicker.Moments") @@ -6333,6 +6477,7 @@ public final class PresentationStrings { self._GroupInfo_InvitationLinkAcceptChannel_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAcceptChannel) self.Conversation_EncryptionCanceled = getValue(dict, "Conversation.EncryptionCanceled") self.AccessDenied_SaveMedia = getValue(dict, "AccessDenied.SaveMedia") + self.InviteText_URL = getValue(dict, "InviteText.URL") self._Channel_AdminLog_MessageInvitedNameUsername = getValue(dict, "Channel.AdminLog.MessageInvitedNameUsername") self._Channel_AdminLog_MessageInvitedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedNameUsername) self.Channel_Username_InvalidTooManyUsernames = getValue(dict, "Channel.Username.InvalidTooManyUsernames") @@ -6581,6 +6726,7 @@ public final class PresentationStrings { self.Your_cards_number_is_invalid = getValue(dict, "Your_cards_number_is_invalid") self._MESSAGE_INVOICE = getValue(dict, "MESSAGE_INVOICE") self._MESSAGE_INVOICE_r = extractArgumentRanges(self._MESSAGE_INVOICE) + self.Localization_LanguageCustom = getValue(dict, "Localization.LanguageCustom") self._Channel_AdminLog_MessageRemovedChannelUsername = getValue(dict, "Channel.AdminLog.MessageRemovedChannelUsername") self._Channel_AdminLog_MessageRemovedChannelUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedChannelUsername) self.Group_MessagePhotoRemoved = getValue(dict, "Group.MessagePhotoRemoved") @@ -6593,6 +6739,7 @@ public final class PresentationStrings { self.ChatSettings_ConnectionType_UseSocks5 = getValue(dict, "ChatSettings.ConnectionType.UseSocks5") self.MediaPicker_MomentsDateRangeYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeYearFormat") self.Cache_ClearNone = getValue(dict, "Cache.ClearNone") + self.SecretTimer_VideoDescription = getValue(dict, "SecretTimer.VideoDescription") self.Login_InvalidCodeError = getValue(dict, "Login.InvalidCodeError") self.Contacts_contacts = getValue(dict, "Contacts.contacts") self.Channel_BanList_BlockedTitle = getValue(dict, "Channel.BanList.BlockedTitle") @@ -6692,6 +6839,8 @@ public final class PresentationStrings { self.SearchImages_ErrorDownloadingImage = getValue(dict, "SearchImages.ErrorDownloadingImage") self._PINNED_GIF = getValue(dict, "PINNED_GIF") self._PINNED_GIF_r = extractArgumentRanges(self._PINNED_GIF) + self._InviteText_SingleContact = getValue(dict, "InviteText.SingleContact") + self._InviteText_SingleContact_r = extractArgumentRanges(self._InviteText_SingleContact) self.Channel_EditAdmin_CannotEdit = getValue(dict, "Channel.EditAdmin.CannotEdit") self.Profile_PhonebookAccessDisabled = getValue(dict, "Profile.PhonebookAccessDisabled") self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") @@ -6702,6 +6851,7 @@ public final class PresentationStrings { self.Notifications_GroupNotificationsAlert = getValue(dict, "Notifications.GroupNotificationsAlert") self.Paint_Masks = getValue(dict, "Paint.Masks") self.StickerPack_ErrorNotFound = getValue(dict, "StickerPack.ErrorNotFound") + self.SecretTimer_ImageDescription = getValue(dict, "SecretTimer.ImageDescription") self._PINNED_CONTACT = getValue(dict, "PINNED_CONTACT") self._PINNED_CONTACT_r = extractArgumentRanges(self._PINNED_CONTACT) self._Conversation_ForwardToGroupFormat = getValue(dict, "Conversation.ForwardToGroupFormat") @@ -6766,6 +6916,8 @@ public final class PresentationStrings { self.Watch_Location_Current = getValue(dict, "Watch.Location.Current") self.Map_MapTitle = getValue(dict, "Map.MapTitle") self.Checkout_NewCard_SaveInfoHelp = getValue(dict, "Checkout.NewCard.SaveInfoHelp") + self._Settings_ApplyProxyAlertCredentials = getValue(dict, "Settings.ApplyProxyAlertCredentials") + self._Settings_ApplyProxyAlertCredentials_r = extractArgumentRanges(self._Settings_ApplyProxyAlertCredentials) self.MediaPicker_CameraRoll = getValue(dict, "MediaPicker.CameraRoll") self._TwoStepAuth_RecoverySent = getValue(dict, "TwoStepAuth.RecoverySent") self._TwoStepAuth_RecoverySent_r = extractArgumentRanges(self._TwoStepAuth_RecoverySent) @@ -6825,6 +6977,8 @@ public final class PresentationStrings { self.Login_InfoLastNamePlaceholder = getValue(dict, "Login.InfoLastNamePlaceholder") self.Contacts_InvitationText = getValue(dict, "Contacts.InvitationText") self.Channel_Members_AddMembersHelp = getValue(dict, "Channel.Members.AddMembersHelp") + self._MESSAGE_VIDEO_SECRET = getValue(dict, "MESSAGE_VIDEO_SECRET") + self._MESSAGE_VIDEO_SECRET_r = extractArgumentRanges(self._MESSAGE_VIDEO_SECRET) self.ReportPeer_Report = getValue(dict, "ReportPeer.Report") self.Channel_EditMessageErrorGeneric = getValue(dict, "Channel.EditMessageErrorGeneric") self.LoginPassword_FloodError = getValue(dict, "LoginPassword.FloodError") @@ -6917,6 +7071,7 @@ public final class PresentationStrings { self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) self.MessageTimer_Title = getValue(dict, "MessageTimer.Title") self.Watch_Compose_Send = getValue(dict, "Watch.Compose.Send") + self.SocksProxySetup_UseForCallsHelp = getValue(dict, "SocksProxySetup.UseForCallsHelp") self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress") self.Settings_BlockedUsers = getValue(dict, "Settings.BlockedUsers") self.Month_ShortAugust = getValue(dict, "Month.ShortAugust") @@ -7132,6 +7287,12 @@ public final class PresentationStrings { self._SharedMedia_Generic_few = getValueWithForm(dict, "SharedMedia.Generic", .few) self._SharedMedia_Generic_many = getValueWithForm(dict, "SharedMedia.Generic", .many) self._SharedMedia_Generic_other = getValueWithForm(dict, "SharedMedia.Generic", .other) + self._InviteText_ContactsCount_zero = getValueWithForm(dict, "InviteText.ContactsCount", .zero) + self._InviteText_ContactsCount_one = getValueWithForm(dict, "InviteText.ContactsCount", .one) + self._InviteText_ContactsCount_two = getValueWithForm(dict, "InviteText.ContactsCount", .two) + self._InviteText_ContactsCount_few = getValueWithForm(dict, "InviteText.ContactsCount", .few) + self._InviteText_ContactsCount_many = getValueWithForm(dict, "InviteText.ContactsCount", .many) + self._InviteText_ContactsCount_other = getValueWithForm(dict, "InviteText.ContactsCount", .other) self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) @@ -7420,6 +7581,12 @@ public final class PresentationStrings { self._AttachmentMenu_SendItem_few = getValueWithForm(dict, "AttachmentMenu.SendItem", .few) self._AttachmentMenu_SendItem_many = getValueWithForm(dict, "AttachmentMenu.SendItem", .many) self._AttachmentMenu_SendItem_other = getValueWithForm(dict, "AttachmentMenu.SendItem", .other) + self._Contacts_ImportersCount_zero = getValueWithForm(dict, "Contacts.ImportersCount", .zero) + self._Contacts_ImportersCount_one = getValueWithForm(dict, "Contacts.ImportersCount", .one) + self._Contacts_ImportersCount_two = getValueWithForm(dict, "Contacts.ImportersCount", .two) + self._Contacts_ImportersCount_few = getValueWithForm(dict, "Contacts.ImportersCount", .few) + self._Contacts_ImportersCount_many = getValueWithForm(dict, "Contacts.ImportersCount", .many) + self._Contacts_ImportersCount_other = getValueWithForm(dict, "Contacts.ImportersCount", .other) self._Watch_UserInfo_Mute_zero = getValueWithForm(dict, "Watch.UserInfo.Mute", .zero) self._Watch_UserInfo_Mute_one = getValueWithForm(dict, "Watch.UserInfo.Mute", .one) self._Watch_UserInfo_Mute_two = getValueWithForm(dict, "Watch.UserInfo.Mute", .two) diff --git a/TelegramUI/RadialProgressContentNode.swift b/TelegramUI/RadialProgressContentNode.swift new file mode 100644 index 0000000000..f387b10bde --- /dev/null +++ b/TelegramUI/RadialProgressContentNode.swift @@ -0,0 +1,283 @@ +import Foundation +import Display +import AsyncDisplayKit +import LegacyComponents + +private final class RadialProgressContentCancelNodeParameters: NSObject { + let color: UIColor + let displayCancel: Bool + + init(color: UIColor, displayCancel: Bool) { + self.color = color + self.displayCancel = displayCancel + } +} + +private final class RadialProgressContentSpinnerNodeParameters: NSObject { + let color: UIColor + let progress: CGFloat + + init(color: UIColor, progress: CGFloat) { + self.color = color + self.progress = progress + } +} + +private final class RadialProgressContentSpinnerNode: ASDisplayNode { + var progressAnimationCompleted: (() -> Void)? + + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + private var effectiveProgress: CGFloat = 0.0 { + didSet { + self.setNeedsDisplay() + } + } + + var progress: CGFloat? { + didSet { + self.pop_removeAnimation(forKey: "progress") + if let progress = self.progress { + self.pop_removeAnimation(forKey: "indefiniteProgress") + + let animation = POPBasicAnimation() + animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in + property?.readBlock = { node, values in + values?.pointee = (node as! RadialProgressContentSpinnerNode).effectiveProgress + } + property?.writeBlock = { node, values in + (node as! RadialProgressContentSpinnerNode).effectiveProgress = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty + animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber + animation.toValue = CGFloat(progress) as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + animation.duration = 0.2 + animation.completionBlock = { [weak self] _ in + self?.progressAnimationCompleted?() + } + self.pop_add(animation, forKey: "progress") + } else if self.pop_animation(forKey: "indefiniteProgress") == nil { + let animation = POPBasicAnimation() + animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in + property?.readBlock = { node, values in + values?.pointee = (node as! RadialProgressContentSpinnerNode).effectiveProgress + } + property?.writeBlock = { node, values in + (node as! RadialProgressContentSpinnerNode).effectiveProgress = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty + animation.fromValue = CGFloat(0.0) as NSNumber + animation.toValue = CGFloat(2.0) as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + animation.duration = 2.5 + animation.repeatForever = true + self.pop_add(animation, forKey: "indefiniteProgress") + } + } + } + + var isAnimatingProgress: Bool { + return self.pop_animation(forKey: "progress") != nil + } + + init(color: UIColor) { + self.color = color + + super.init() + + self.isLayerBacked = true + self.displaysAsynchronously = true + self.isOpaque = false + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialProgressContentSpinnerNodeParameters(color: self.color, progress: self.effectiveProgress) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialProgressContentSpinnerNodeParameters { + context.setStrokeColor(parameters.color.cgColor) + + var progress = parameters.progress + var startAngle = -CGFloat.pi / 2.0 + var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + if progress > 1.0 { + progress = 2.0 - progress + let tmp = startAngle + startAngle = endAngle + endAngle = tmp + } + progress = min(1.0, progress) + + let pathDiameter = bounds.size.width - 2.25 - 2.5 * 2.0 + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle:endAngle, clockwise:true) + path.lineWidth = 2.25; + path.lineCapStyle = .round; + path.stroke() + } + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = 2.0 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + + self.layer.add(basicAnimation, forKey: "progressRotation") + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.layer.removeAnimation(forKey: "progressRotation") + } +} + +private final class RadialProgressContentCancelNode: ASDisplayNode { + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + let displayCancel: Bool + + init(color: UIColor, displayCancel: Bool) { + self.color = color + self.displayCancel = displayCancel + + super.init() + + self.isLayerBacked = true + self.displaysAsynchronously = true + self.isOpaque = false + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialProgressContentCancelNodeParameters(color: self.color, displayCancel: self.displayCancel) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialProgressContentCancelNodeParameters { + if parameters.displayCancel { + let diameter = min(bounds.size.width, bounds.size.height) + context.setStrokeColor(parameters.color.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + + let crossSize: CGFloat = 14.0 + context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + } + } + } +} + +final class RadialProgressContentNode: RadialStatusContentNode { + private let spinnerNode: RadialProgressContentSpinnerNode + private let cancelNode: RadialProgressContentCancelNode + + var color: UIColor { + didSet { + self.setNeedsDisplay() + self.spinnerNode.color = self.color + } + } + + var progress: CGFloat? = 0.0 { + didSet { + self.spinnerNode.progress = self.progress + } + } + + let displayCancel: Bool + + private var enqueuedReadyForTransition: (() -> Void)? + + init(color: UIColor, displayCancel: Bool) { + self.color = color + self.displayCancel = displayCancel + + self.spinnerNode = RadialProgressContentSpinnerNode(color: color) + self.cancelNode = RadialProgressContentCancelNode(color: color, displayCancel: displayCancel) + + super.init() + + self.isLayerBacked = true + + self.addSubnode(self.spinnerNode) + self.addSubnode(self.cancelNode) + + self.spinnerNode.progressAnimationCompleted = { [weak self] in + if let strongSelf = self { + if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition { + strongSelf.enqueuedReadyForTransition = nil + enqueuedReadyForTransition() + } + } + } + } + + override func enqueueReadyForTransition(_ f: @escaping () -> Void) { + if self.spinnerNode.isAnimatingProgress { + self.enqueuedReadyForTransition = f + } else { + f() + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + self.spinnerNode.bounds = bounds + self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0) + self.cancelNode.frame = bounds + } + + override func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) + } + + override func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15) + } +} diff --git a/TelegramUI/RadialProgressNode.swift b/TelegramUI/RadialProgressNode.swift index a1b22e2415..21b29bb627 100644 --- a/TelegramUI/RadialProgressNode.swift +++ b/TelegramUI/RadialProgressNode.swift @@ -2,7 +2,7 @@ import Foundation import AsyncDisplayKit import SwiftSignalKit import Display -import TelegramLegacyComponents +import LegacyComponents private class RadialProgressParameters: NSObject { let theme: RadialProgressTheme @@ -95,7 +95,7 @@ private class RadialProgressOverlayNode: ASDisplayNode { return RadialProgressOverlayParameters(theme: self.theme, diameter: self.frame.size.width, state: updatedState) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { @@ -114,7 +114,7 @@ private class RadialProgressOverlayNode: ASDisplayNode { break case let .Fetching(progress): let startAngle = -CGFloat.pi / 2.0 - let endAngle = 2.0 * (CGFloat.pi / 2.0) * CGFloat(progress) - CGFloat(M_PI_2) + let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle let pathDiameter = parameters.diameter - 2.25 - 2.5 * 2.0 @@ -274,7 +274,7 @@ class RadialProgressNode: ASControlNode { return RadialProgressParameters(theme: self.theme, diameter: self.frame.size.width, state: self.state) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { diff --git a/TelegramUI/RadialStatusBackgroundNode.swift b/TelegramUI/RadialStatusBackgroundNode.swift new file mode 100644 index 0000000000..e7d9bb55e5 --- /dev/null +++ b/TelegramUI/RadialStatusBackgroundNode.swift @@ -0,0 +1,46 @@ +import Foundation +import AsyncDisplayKit + +private final class RadialStatusBackgroundNodeParameters: NSObject { + let color: UIColor + + init(color: UIColor) { + self.color = color + } +} + +final class RadialStatusBackgroundNode: ASDisplayNode { + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + init(color: UIColor) { + self.color = color + + super.init() + + self.isLayerBacked = true + self.isOpaque = false + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialStatusBackgroundNodeParameters(color: self.color) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialStatusBackgroundNodeParameters { + context.setFillColor(parameters.color.cgColor) + context.fillEllipse(in: bounds) + } + } +} diff --git a/TelegramUI/RadialStatusContentNode.swift b/TelegramUI/RadialStatusContentNode.swift new file mode 100644 index 0000000000..ec8b82b3f4 --- /dev/null +++ b/TelegramUI/RadialStatusContentNode.swift @@ -0,0 +1,21 @@ +import Foundation +import Display +import AsyncDisplayKit + +class RadialStatusContentNode: ASDisplayNode { + func enqueueReadyForTransition(_ f: @escaping () -> Void) { + f() + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15) + } +} diff --git a/TelegramUI/RadialStatusIconContentNode.swift b/TelegramUI/RadialStatusIconContentNode.swift new file mode 100644 index 0000000000..01dbafd8a9 --- /dev/null +++ b/TelegramUI/RadialStatusIconContentNode.swift @@ -0,0 +1,109 @@ +import Foundation +import Display +import AsyncDisplayKit + +enum RadialStatusIcon { + case custom(UIImage) + case play(UIColor) + case pause(UIColor) + case download(UIColor) +} + +private final class RadialStatusIconContentNodeParameters: NSObject { + let icon: RadialStatusIcon + + init(icon: RadialStatusIcon) { + self.icon = icon + + super.init() + } +} + +final class RadialStatusIconContentNode: RadialStatusContentNode { + private let icon: RadialStatusIcon + + init(icon: RadialStatusIcon) { + self.icon = icon + + super.init() + + self.isLayerBacked = true + self.isOpaque = false + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialStatusIconContentNodeParameters(icon: self.icon) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialStatusIconContentNodeParameters { + let diameter = min(bounds.size.width, bounds.size.height) + switch parameters.icon { + case let .download(color): + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + + let arrowHeadSize: CGFloat = 15.0 + let arrowLength: CGFloat = 18.0 + let arrowHeadOffset: CGFloat = 1.0 + + context.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0 + arrowHeadOffset)) + context.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - 1.0 + arrowHeadOffset)) + context.strokePath() + + context.move(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + context.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) + context.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + context.strokePath() + case let .play(color): + context.setFillColor(color.cgColor) + + let size = CGSize(width: 15.0, height: 18.0) + context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 0.8, y: 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") + context.fillPath() + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0) + case let .pause(color): + context.setFillColor(color.cgColor) + + let size = CGSize(width: 15.0, height: 16.0) + context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 0.8, y: 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ") + context.fillPath() + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0) + case let .custom(image): + image.draw(at: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0))) + } + } + } +} diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift new file mode 100644 index 0000000000..6f051f59f3 --- /dev/null +++ b/TelegramUI/RadialStatusNode.swift @@ -0,0 +1,186 @@ +import Foundation +import AsyncDisplayKit + +enum RadialStatusNodeState: Equatable { + case none + case download(UIColor) + case play(UIColor) + case pause(UIColor) + case progress(color: UIColor, value: CGFloat?, cancelEnabled: Bool) + case customIcon(UIImage) + + static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .download(lhsColor): + if case let .download(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + return true + } else { + return false + } + case let .play(lhsColor): + if case let .play(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + return true + } else { + return false + } + case let .pause(lhsColor): + if case let .pause(rhsColor) = rhs, lhsColor.isEqual(rhsColor) { + return true + } else { + return false + } + case let .progress(lhsColor, lhsValue, lhsCancelEnabled): + if case let .progress(rhsColor, rhsValue, rhsCancelEnabled) = rhs, lhsColor.isEqual(rhsColor), lhsValue == rhsValue, lhsCancelEnabled == rhsCancelEnabled { + return true + } else { + return false + } + case let .customIcon(lhsImage): + if case let .customIcon(rhsImage) = rhs, lhsImage === rhsImage { + return true + } else { + return false + } + } + } + + func backgroundColor(color: UIColor) -> UIColor? { + switch self { + case .none: + return nil + default: + return color + } + } + + func contentNode(current: RadialStatusContentNode?) -> RadialStatusContentNode? { + switch self { + case .none: + return nil + case let .download(color): + return RadialStatusIconContentNode(icon: .download(color)) + case let .play(color): + return RadialStatusIconContentNode(icon: .play(color)) + case let .pause(color): + return RadialStatusIconContentNode(icon: .pause(color)) + case let .customIcon(image): + return RadialStatusIconContentNode(icon: .custom(image)) + case let .progress(color, value, cancelEnabled): + if let current = current as? RadialProgressContentNode, current.displayCancel == cancelEnabled { + if !current.color.isEqual(color) { + current.color = color + } + current.progress = value + return current + } else { + let node = RadialProgressContentNode(color: color, displayCancel: cancelEnabled) + node.progress = value + return node + } + } + } +} + +final class RadialStatusNode: ASControlNode { + private var backgroundNodeColor: UIColor + + private var state: RadialStatusNodeState = .none + + private var backgroundNode: RadialStatusBackgroundNode? + private var contentNode: RadialStatusContentNode? + private var nextContentNode: RadialStatusContentNode? + + init(backgroundNodeColor: UIColor) { + self.backgroundNodeColor = backgroundNodeColor + + super.init() + } + + func transitionToState(_ state: RadialStatusNodeState, completion: @escaping () -> Void) { + if self.state != state { + self.state = state + + let contentNode = state.contentNode(current: self.contentNode) + if contentNode !== self.contentNode { + self.transitionToContentNode(contentNode, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + } else { + self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + } + } + } + + private func transitionToContentNode(_ node: RadialStatusContentNode?, backgroundColor: UIColor?, completion: @escaping () -> Void) { + if let contentNode = self.contentNode { + self.nextContentNode = node + contentNode.enqueueReadyForTransition { [weak contentNode, weak self] in + if let strongSelf = self, let contentNode = contentNode, strongSelf.contentNode === contentNode { + contentNode.animateOut { [weak contentNode] in + contentNode?.removeFromSupernode() + } + strongSelf.contentNode = strongSelf.nextContentNode + if let contentNode = strongSelf.contentNode { + strongSelf.addSubnode(contentNode) + contentNode.frame = strongSelf.bounds + contentNode.layout() + contentNode.animateIn() + } + strongSelf.transitionToBackgroundColor(backgroundColor, completion: completion) + } + } + } else { + self.contentNode = node + if let contentNode = self.contentNode { + contentNode.frame = self.bounds + self.addSubnode(contentNode) + } + self.transitionToBackgroundColor(backgroundColor, completion: completion) + } + } + + private func transitionToBackgroundColor(_ color: UIColor?, completion: @escaping () -> Void) { + let currentColor = self.backgroundNode?.color + + var updated = false + if let color = color, let currentColor = currentColor { + updated = !color.isEqual(currentColor) + } else if (currentColor != nil) != (color != nil) { + updated = true + } + + if updated { + if let color = color { + if let backgroundNode = self.backgroundNode { + backgroundNode.color = color + completion() + } else { + let backgroundNode = RadialStatusBackgroundNode(color: color) + backgroundNode.frame = self.bounds + self.backgroundNode = backgroundNode + self.insertSubnode(backgroundNode, at: 0) + completion() + } + } else if let backgroundNode = self.backgroundNode { + self.backgroundNode = nil + backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in + backgroundNode?.removeFromSupernode() + completion() + }) + } + } else { + completion() + } + } + + override func layout() { + self.backgroundNode?.frame = self.bounds + if let contentNode = self.contentNode { + contentNode.frame = self.bounds + } + } +} diff --git a/TelegramUI/RadialTimeoutNode.swift b/TelegramUI/RadialTimeoutNode.swift index 313ac1af56..045e6029bb 100644 --- a/TelegramUI/RadialTimeoutNode.swift +++ b/TelegramUI/RadialTimeoutNode.swift @@ -79,7 +79,7 @@ public final class RadialTimeoutNode: ASDisplayNode { return RadialTimeoutNodeParameters(backgroundColor: self.nodeBackgroundColor, foregroundColor: self.nodeForegroundColor, value: value) } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! if !isRasterizing { @@ -98,7 +98,7 @@ public final class RadialTimeoutNode: ASDisplayNode { let radius = (bounds.size.width - 4.0) * 0.5 let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5) - var startAngle = -CGFloat.pi * 0.5 + let startAngle = -CGFloat.pi * 0.5 // update the end angle of the segment let endAngle = startAngle + 2.0 * CGFloat.pi * parameters.value diff --git a/Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg b/TelegramUI/Resources/ChatWallpaperBuiltin0.jpg similarity index 100% rename from Images.xcassets/Chat/Wallpapers/Builtin0.imageset/Dogs BG.jpg rename to TelegramUI/Resources/ChatWallpaperBuiltin0.jpg diff --git a/TelegramUI/SecretMediaPreviewControllerNode.swift b/TelegramUI/SecretMediaPreviewControllerNode.swift index ec38e17efe..2bf366e391 100644 --- a/TelegramUI/SecretMediaPreviewControllerNode.swift +++ b/TelegramUI/SecretMediaPreviewControllerNode.swift @@ -15,9 +15,11 @@ class SecretMediaPreviewControllerNode: ASDisplayNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = UIColor.black - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.addSubnode(self.backgroundNode) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index c3bea4cbd3..8566620a72 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private struct SettingsItemArguments { let account: Account @@ -425,21 +425,20 @@ public func settingsController(account: Account, accountManager: AccountManager) })) }) }, changeProfilePhoto: { - let emptyController = LegacyEmptyController() + let legacyController = LegacyController(presentation: .custom) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) + legacyController.bind(controller: navigationController) presentControllerImpl?(legacyController, nil) - let mixin = TGMediaAvatarMenuMixin(parentController: emptyController, hasDeleteButton: false, personalPhoto: true)! - mixin.applicationInterface = legacyController.applicationInterface + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! let _ = currentAvatarMixin.swap(mixin) - mixin.didDismiss = { [weak legacyController] in - legacyController?.dismiss() - } mixin.didFinishWithImage = { image in if let image = image { if let data = UIImageJPEGRepresentation(image, 0.6) { @@ -466,7 +465,12 @@ public func settingsController(account: Account, accountManager: AccountManager) let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } - mixin.present() + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } }, openPrivacyAndSecurity: { pushControllerImpl?(privacyAndSecurityController(account: account, initialSettings: .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map { Optional($0) }))) }, openDataAndStorage: { diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 57309061a6..d1a62ec73e 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -50,16 +50,14 @@ public final class ShareController: ViewController { super.init(navigationBarTheme: nil) - self.peers.set(account.postbox.tailChatListView(100) |> take(1) |> map { view -> [Peer] in + self.peers.set(account.viewTracker.tailChatListView(count: 100) |> take(1) |> map { view -> [Peer] in var peers: [Peer] = [] for entry in view.0.entries.reversed() { switch entry { - case let .MessageEntry(_, message, _, _, _, renderedPeer): - if let message = message { - if let peer = message.peers[message.id.peerId] { - if canSendMessagesToPeer(peer) { - peers.append(peer) - } + case let .MessageEntry(_, message, _, _, _, renderedPeer, _): + if let peer = renderedPeer.chatMainPeer { + if canSendMessagesToPeer(peer) { + peers.append(peer) } } default: diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index aaa76f0032..c3638d1216 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -306,7 +306,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) - self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) if animateIn { diff --git a/TelegramUI/SoftwareVideoSource.swift b/TelegramUI/SoftwareVideoSource.swift index 32812f07f0..e91647933b 100644 --- a/TelegramUI/SoftwareVideoSource.swift +++ b/TelegramUI/SoftwareVideoSource.swift @@ -135,6 +135,12 @@ final class SoftwareVideoSource { } deinit { + if let avIoContext = self.avIoContext { + if avIoContext.pointee.buffer != nil { + av_free(avIoContext.pointee.buffer) + } + av_free(avIoContext) + } if let avFormatContext = self.avFormatContext { avformat_free_context(avFormatContext) } @@ -179,7 +185,7 @@ final class SoftwareVideoSource { duration = videoStream.fps } - let frame = MediaTrackDecodableFrame(type: .video, packet: &packet.packet, pts: pts, dts: dts, duration: duration) + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) frames.append(frame) } } else { diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 89e90c5433..1d04de5800 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -246,8 +246,6 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } - //self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: nil, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - let titleSize = self.contentTitleNode.measure(contentContainerFrame.size) let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize) let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY) @@ -278,7 +276,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) - self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) if animateIn { diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index dc7931bd19..fd5718d850 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -93,7 +93,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { var text = "" for attribute in stickerItem.file.attributes { - if case let .Sticker(displayText, _) = attribute { + if case let .Sticker(displayText, _, _) = attribute { text = displayText break } diff --git a/TelegramUI/StickerPreviewControllerNode.swift b/TelegramUI/StickerPreviewControllerNode.swift index 28a9f9e679..f6dd4772c4 100644 --- a/TelegramUI/StickerPreviewControllerNode.swift +++ b/TelegramUI/StickerPreviewControllerNode.swift @@ -29,9 +29,11 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.imageNode = TransformImageNode() self.imageNode.addSubnode(self.textNode) - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.addSubnode(self.dimNode) self.addSubnode(self.imageNode) @@ -129,7 +131,7 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.item = item - for case let .Sticker(text, _) in item.file.attributes { + for case let .Sticker(text, _, _) in item.file.attributes { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) break } diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 7cbb9d4028..1b281ba7a8 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -74,7 +74,71 @@ private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, } } -func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +func chatMessageLegacySticker(account: Account, file: TelegramMediaFile, small: Bool, fitSize: CGSize, fetched: Bool = false, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageStickerDatas(account: account, file: file, small: small, fetched: fetched) + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return { preArguments in + var fullSizeImage: (UIImage, UIImage)? + if let fullSizeData = fullSizeData, fullSizeComplete { + if let image = imageFromAJpeg(data: fullSizeData) { + fullSizeImage = image + } + } + + if let fullSizeImage = fullSizeImage { + var updatedFitSize = fitSize + if updatedFitSize.width.isEqual(to: 1.0) { + updatedFitSize = fullSizeImage.0.size + } + + let contextSize = fullSizeImage.0.size.aspectFitted(updatedFitSize) + + let arguments = TransformImageArguments(corners: preArguments.corners, imageSize: contextSize, boundingSize: contextSize, intrinsicInsets: preArguments.intrinsicInsets) + + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let thumbnailImage: CGImage? = nil + + var blurredThumbnailImage: UIImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(blurredThumbnailImage.cgImage!, in: arguments.drawingRect) + } + + if let cgImage = fullSizeImage.0.cgImage, let cgImageAlpha = fullSizeImage.1.cgImage { + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + let mask = CGImage(maskWidth: cgImageAlpha.width, height: cgImageAlpha.height, bitsPerComponent: cgImageAlpha.bitsPerComponent, bitsPerPixel: cgImageAlpha.bitsPerPixel, bytesPerRow: cgImageAlpha.bytesPerRow, provider: cgImageAlpha.dataProvider!, decode: nil, shouldInterpolate: true) + + c.draw(cgImage.masking(mask!)!, in: arguments.drawingRect) + } + } + + return context + } else { + return nil + } + } + } +} + +func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatMessageStickerDatas(account: account, file: file, small: small, fetched: fetched) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 81915d1ae8..a781e7703a 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -35,26 +35,42 @@ public final class TelegramApplicationContext { return self._presentationData.get() } + public let currentAutomaticMediaDownloadSettings: Atomic + private let _automaticMediaDownloadSettings = Promise() + public var automaticMediaDownloadSettings: Signal { + return self._automaticMediaDownloadSettings.get() + } + private let presentationDataDisposable = MetaDisposable() + private let automaticMediaDownloadSettingsDisposable = MetaDisposable() public var navigateToCurrentCall: (() -> Void)? public var hasOngoingCall: Signal? - public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal) { + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal) { self.applicationBindings = applicationBindings self.accountManager = accountManager self.currentPresentationData = Atomic(value: currentPresentationData) + self.currentAutomaticMediaDownloadSettings = Atomic(value: currentMediaDownloadSettings) self._presentationData.set(.single(currentPresentationData) |> then(presentationData)) + self._automaticMediaDownloadSettings.set(.single(currentMediaDownloadSettings) |> then(automaticMediaDownloadSettings)) self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in if let strongSelf = self { let _ = strongSelf.currentPresentationData.swap(next) } })) + + self.automaticMediaDownloadSettingsDisposable.set(self._automaticMediaDownloadSettings.get().start(next: { [weak self] next in + if let strongSelf = self { + let _ = strongSelf.currentAutomaticMediaDownloadSettings.swap(next) + } + })) } deinit { self.presentationDataDisposable.dispose() + self.automaticMediaDownloadSettingsDisposable.dispose() } public func attachOverlayMediaController(_ controller: OverlayMediaController) { diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 8346d8f4e4..6d8d672a32 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -1,54 +1,262 @@ import Foundation -import TelegramLegacyComponents +import LegacyComponents import UIKit +import TelegramCore +import SwiftSignalKit +import MtProtoKitDynamic -/* - [TGHacks setApplication:application]; - [TGHacks setCurrentSizeClassGetter:^UIUserInterfaceSizeClass{ - return TGAppDelegateInstance.rootController.currentSizeClass; - }]; - [TGHacks setCurrenHorizontalClassGetter:^UIUserInterfaceSizeClass{ - return TGAppDelegateInstance.rootController.traitCollection.horizontalSizeClass; - }]; - TGLegacyComponentsSetDocumentsPath([TGAppDelegate documentsPath]); - [TGHacks setForceSetStatusBarHidden:^(BOOL hidden, UIStatusBarAnimation animation) { - [(TGApplication *)[UIApplication sharedApplication] forceSetStatusBarHidden:hidden withAnimation:animation]; - }]; - [TGHacks setApplicationBounds:^CGRect { - return TGAppDelegateInstance.rootController.applicationBounds; - }]; - [TGHacks setPauseMusicPlayer:^{ - [TGTelegraphInstance.musicPlayer controlPause]; - }]; - TGLegacyComponentsSetAccessChecker([[TGAccessCheckerImpl alloc] init]); - */ +var legacyComponentsApplication: UIApplication! -private final class AccessCheckerImpl: NSObject, TGAccessCheckerProtocol { - func checkAddressBookAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { +private let legacyLocalization = TGLocalization(version: 0, code: "en", dict: [:], isActive: true) +private var legacyDocumentsStorePath: String? +private var legacyCanOpenUrl: (URL) -> Bool = { _ in return false } +private var legacyOpenUrl: (URL) -> Void = { _ in } +private weak var legacyAccount: Account? + +func legacyAccountGet() -> Account? { + return legacyAccount +} + +private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponentsAccessChecker { + public func checkAddressBookAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { return true } - func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + public func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { return true } - func checkMicrophoneAuthorizationStatus(for intent: TGMicrophoneAccessIntent, alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + public func checkMicrophoneAuthorizationStatus(for intent: TGMicrophoneAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { return true } - func checkCameraAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + public func checkCameraAuthorizationStatus(for intent: TGCameraAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { return true } - func checkLocationAuthorizationStatus(for intent: TGLocationAccessIntent, alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + public func checkLocationAuthorizationStatus(for intent: TGLocationAccessIntent, alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { return true } } +private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyComponentsGlobalsProvider { + func log(_ string: String!) { + print(string) + } + + public func effectiveLocalization() -> TGLocalization! { + return legacyLocalization + } + + public func applicationWindows() -> [UIWindow]! { + return legacyComponentsApplication.windows + } + + public func applicationStatusBarWindow() -> UIWindow! { + return nil + } + + public func applicationKeyboardWindow() -> UIWindow! { + return nil + } + + public func applicationInstance() -> UIApplication! { + return legacyComponentsApplication + } + + public func applicationStatusBarOrientation() -> UIInterfaceOrientation { + return legacyComponentsApplication.statusBarOrientation + } + + public func statusBarFrame() -> CGRect { + return legacyComponentsApplication.statusBarFrame + } + + public func isStatusBarHidden() -> Bool { + return false + } + + public func setStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation) { + } + + public func statusBarStyle() -> UIStatusBarStyle { + return .default + } + + public func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle, animated: Bool) { + + } + + public func forceStatusBarAppearanceUpdate() { + + } + + public func canOpen(_ url: URL!) -> Bool { + return legacyCanOpenUrl(url) + } + + public func open(_ url: URL!) { + legacyOpenUrl(url) + } + + public func openURLNative(_ url: URL!) { + legacyOpenUrl(url) + } + + public func disableUserInteraction(for timeInterval: TimeInterval) { + } + + public func setIdleTimerDisabled(_ value: Bool) { + legacyComponentsApplication.isIdleTimerDisabled = value + } + + public func pauseMusicPlayback() { + } + + public func dataStoragePath() -> String! { + return legacyDocumentsStorePath! + } + + public func dataCachePath() -> String! { + return legacyDocumentsStorePath! + "/Cache" + } + + public func accessChecker() -> LegacyComponentsAccessChecker! { + return LegacyComponentsAccessCheckerImpl() + } + + public func stickerPacksSignal() -> SSignal! { + if let legacyAccount = legacyAccount { + return legacyComponentsStickers(postbox: legacyAccount.postbox, namespace: Namespaces.ItemCollection.CloudStickerPacks) + } else { + var dict: [AnyHashable: Any] = [:] + dict["packs"] = NSArray() + return SSignal.single(dict) + } + } + + public func maskStickerPacksSignal() -> SSignal! { + if let legacyAccount = legacyAccount { + return legacyComponentsStickers(postbox: legacyAccount.postbox, namespace: Namespaces.ItemCollection.CloudMaskPacks) + } else { + var dict: [AnyHashable: Any] = [:] + dict["packs"] = NSArray() + return SSignal.single(dict) + } + } + + public func recentStickerMasksSignal() -> SSignal! { + return SSignal.single(NSArray()) + } + + public func request(_ type: TGAudioSessionType, interrupted: (() -> Void)!) -> SDisposable! { + return nil + } + + public func currentWallpaperInfo() -> TGWallpaperInfo! { + return nil + } + + public func currentWallpaperImage() -> UIImage! { + return nil + } + + public func sharedMediaImageProcessingThreadPool() -> SThreadPool! { + return nil + } + + public func sharedMediaMemoryImageCache() -> TGMemoryImageCache! { + return nil + } + + public func squarePhotoThumbnail(_ imageAttachment: TGImageMediaAttachment!, of size: CGSize, threadPool: SThreadPool!, memoryCache: TGMemoryImageCache!, pixelProcessingBlock: ((UnsafeMutableRawPointer?, Int32, Int32, Int32) -> Void)!, downloadLargeImage: Bool, placeholder: SSignal!) -> SSignal! { + return SSignal.never() + } + + public func localDocumentDirectory(forLocalDocumentId localDocumentId: Int64, version: Int32) -> String! { + return "" + } + + public func localDocumentDirectory(forDocumentId documentId: Int64, version: Int32) -> String! { + return "" + } + + public func json(forHttpLocation httpLocation: String!) -> SSignal! { + return self.data(forHttpLocation: httpLocation).map(toSignal: { next in + if let next = next as? Data { + if let object = try? JSONSerialization.jsonObject(with: next, options: []) { + return SSignal.single(object) + } + } + return SSignal.fail(nil) + }) + } + + public func data(forHttpLocation httpLocation: String!) -> SSignal! { + return SSignal { subscriber in + if let httpLocation = httpLocation, let url = URL(string: httpLocation) { + let disposable = MTHttpRequestOperation.data(forHttpUrl: url).start(next: { next in + subscriber?.putNext(next) + }, error: { error in + subscriber?.putError(error) + }, completed: { + subscriber?.putCompletion() + }) + return SBlockDisposable(block: { + disposable?.dispose() + }) + } else { + return nil + } + } + } + + public func makeHTTPRequestOperation(with request: URLRequest!) -> Operation! { + return nil + } + + public func pausePictureInPicturePlayback() { + + } + + public func resumePictureInPicturePlayback() { + + } + + public func maybeReleaseVolumeOverlay() { + + } +} + +public func setupLegacyComponents(account: Account) { + legacyAccount = account +} + public func initializeLegacyComponents(application: UIApplication, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect, canOpenUrl: @escaping (URL) -> Bool, openUrl: @escaping (URL) -> Void) { + legacyComponentsApplication = application + legacyCanOpenUrl = canOpenUrl + legacyOpenUrl = openUrl + legacyDocumentsStorePath = documentsPath + freedomInit() + + TGRemoteImageView.setSharedCache(TGCache()) + + TGImageDataSource.register(LegacyStickerImageDataSource(account: { + return legacyAccount + })) + TGImageDataSource.register(LegacyPeerAvatarPlaceholderDataSource(account: { + return legacyAccount + })) + TGImageDataSource.register(LegacyLocationVenueIconDataSource(account: { + return legacyAccount + })) + ASActor.registerClass(LegacyImageDownloadActor.self) + + LegacyComponentsGlobals.setProvider(LegacyComponentsGlobalsProviderImpl()) //freedomUIKitInit(); - TGHacks.setApplication(application) + + /*TGHacks.setApplication(application) TGLegacyComponentsSetAccessChecker(AccessCheckerImpl()) TGHacks.setPauseMusicPlayer { @@ -68,5 +276,5 @@ public func initializeLegacyComponents(application: UIApplication, currentSizeCl if let url = url { return openUrl(url) } - }) + })*/ } diff --git a/TelegramUI/TelegramVideoNode.swift b/TelegramUI/TelegramVideoNode.swift index 5d88e9a10f..3b8972e9c5 100644 --- a/TelegramUI/TelegramVideoNode.swift +++ b/TelegramUI/TelegramVideoNode.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Postbox import TelegramCore -import TelegramLegacyComponents +import LegacyComponents private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdge, view: TGEmbedPIPPullArrowView) { let arrowX: CGFloat diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 8a864d0fba..5938fb4a78 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -358,7 +358,7 @@ final class TextNode: ASDisplayNode { return self.cachedLayout } - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! context.setAllowsAntialiasing(true) diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift index 18b937ea04..d8c776cacd 100644 --- a/TelegramUI/ThemeGridControllerNode.swift +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -51,7 +51,7 @@ private func preparedThemeGridEntryTransition(account: Account, from fromEntries let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction)) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } return ThemeGridEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) } @@ -78,9 +78,11 @@ final class ThemeGridControllerNode: ASDisplayNode { self.gridNode = GridNode() self.gridNode.showVerticalScrollIndicator = true - super.init(viewBlock: { + super.init() + + self.setViewBlock({ return UITracingLayerView() - }, didLoad: nil) + }) self.backgroundColor = presentationData.theme.list.itemBackgroundColor @@ -139,7 +141,7 @@ final class ThemeGridControllerNode: ASDisplayNode { private func dequeueTransitions() { while !self.queuedTransitions.isEmpty { let transition = self.queuedTransitions.removeFirst() - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in if let strongSelf = self { strongSelf.ready.set(true) } @@ -164,7 +166,7 @@ final class ThemeGridControllerNode: ASDisplayNode { insets.top += spacing - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, lineSpacing: spacing)), transition: .immediate), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, lineSpacing: spacing)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) diff --git a/TelegramUI/TransformImageNode.swift b/TelegramUI/TransformImageNode.swift index c34e5d52e2..90a86e45c9 100644 --- a/TelegramUI/TransformImageNode.swift +++ b/TelegramUI/TransformImageNode.swift @@ -55,9 +55,9 @@ public class TransformImageNode: ASDisplayNode { } } - self.disposable.set((result |> deliverOnMainQueue).start(next: {[weak self] next in + self.disposable.set((result |> deliverOnMainQueue).start(next: { [weak self] next in if dispatchOnDisplayLink { - displayLinkDispatcher.dispatch { [weak self] in + displayLinkDispatcher.dispatch { if let strongSelf = self { if strongSelf.alphaTransitionOnFirstUpdate && strongSelf.contents == nil { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) diff --git a/submodules/libtgvoip b/submodules/libtgvoip index c7047d0efe..45f8df3c96 160000 --- a/submodules/libtgvoip +++ b/submodules/libtgvoip @@ -1 +1 @@ -Subproject commit c7047d0efe43fb87fd97cef11b7e48501db10702 +Subproject commit 45f8df3c9635ee48d6595ddb126cd41bbaba28be diff --git a/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift new file mode 100644 index 0000000000..07652f2875 --- /dev/null +++ b/third-party/RMIntro/LegacyLocationVenueIconDataSource.swift @@ -0,0 +1,127 @@ +import Foundation +import LegacyComponents +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private let sharedImageCache = TGMemoryImageCache(softMemoryLimit: 2 * 1024 * 1024, hardMemoryLimit: 3 * 1024 * 1024)! + +private let placeholderImage = generateFilledCircleImage(diameter: 40.0, color: UIColor(rgb: 0xf2f2f2)) + +private final class LegacyLocationVenueIconTask: NSObject { + private let disposable = DisposableSet() + + init(account: Account, url: String, completion: @escaping (Data?) -> Void) { + super.init() + + let resource = HttpReferenceMediaResource(url: url, size: nil) + self.disposable.add(account.postbox.mediaBox.resourceData(resource).start(next: { data in + if data.complete { + if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + completion(loadedData) + } + } + })) + self.disposable.add(account.postbox.mediaBox.fetchedResource(resource, tag: nil).start()) + } + + deinit { + self.disposable.dispose() + } + + func cancel() { + self.disposable.dispose() + } +} + +final class LegacyLocationVenueIconDataSource: TGImageDataSource { + private let account: () -> Account? + + init(account: @escaping () -> Account?) { + self.account = account + + super.init() + } + + override func canHandleUri(_ uri: String!) -> Bool { + if let uri = uri { + if uri.hasPrefix("location-venue-icon://") { + return true + } + } + return false + } + + override func loadAttributeSync(forUri uri: String!, attribute: String!) -> Any! { + if attribute == "placeholder" { + return placeholderImage + } + return nil + } + + override func loadDataSync(withUri uri: String!, canWait: Bool, acceptPartialData: Bool, asyncTaskId: AutoreleasingUnsafeMutablePointer!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> TGDataResource! { + if let image = sharedImageCache.image(forKey: uri, attributes: nil) { + return TGDataResource(image: image, decoded: true) + } + return nil + } + + override func loadDataAsync(withUri uri: String!, progress: ((Float) -> Void)!, partialCompletion: ((TGDataResource?) -> Void)!, completion: ((TGDataResource?) -> Void)!) -> Any! { + if let account = self.account() { + let args: [AnyHashable : Any] + let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "location-venue-icon://".characters.count)) + args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! + + guard let width = Int((args["width"] as! String)), width > 1 else { + return nil + } + guard let height = Int((args["height"] as! String)), height > 1 else { + return nil + } + + guard let url = args["url"] as? String else { + return nil + } + + let size = CGSize(width: CGFloat(width), height: CGFloat(height)) + + return LegacyLocationVenueIconTask(account: account, url: url, completion: { data in + if let data = data, let iconSourceImage = UIImage(data: data) { + UIGraphicsBeginImageContextWithOptions(iconSourceImage.size, false, iconSourceImage.scale) + var context = UIGraphicsGetCurrentContext()! + iconSourceImage.draw(at: CGPoint()) + context.setBlendMode(.sourceAtop) + context.setFillColor(UIColor(rgb: 0xa0a0a0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: iconSourceImage.size)) + + let tintedIconImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + context = UIGraphicsGetCurrentContext()! + context.setFillColor(UIColor(rgb: 0xf2f2f2).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + let imageRect = CGRect(x: 4.0, y: 4.0, width: 32.0, height: 32.0) + tintedIconImage?.draw(in: imageRect) + + let iconImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext() + + if let iconImage = iconImage { + sharedImageCache.setImage(iconImage, forKey: uri, attributes: nil) + completion?(TGDataResource(image: iconImage, decoded: true)) + } + } + }) + } else { + return nil + } + } + + override func cancelTask(byId taskId: Any!) { + if let disposable = taskId as? LegacyLocationVenueIconTask { + disposable.cancel() + } + } +}