From 46295401ff6c98aaf63e991c56fb212d7ee73adc Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 26 Sep 2017 03:01:24 +0300 Subject: [PATCH] no message --- .../ComposeIcon.imageset/Contents.json | 22 + .../ModernNavigationComposeButtonIcon@2x.png | Bin 0 -> 503 bytes .../ModernNavigationComposeButtonIcon@3x.png | Bin 0 -> 701 bytes .../ChannelVerifiedIconSmall@2x.png | Bin 0 -> 538 bytes .../ChannelVerifiedIconSmall@3x.png | Bin 0 -> 781 bytes .../PeerVerifiedIcon.imageset}/Contents.json | 4 +- Images.xcassets/Components/Contents.json | 9 + .../Search Bar/Clear.imageset/Clear.pdf | Bin 0 -> 4177 bytes .../Search Bar/Clear.imageset/Contents.json | 12 + .../Components/Search Bar/Contents.json | 9 + .../Search Bar/Loupe.imageset/Contents.json | 22 + .../Loupe.imageset/SearchBarIconLight@2x.png | Bin 0 -> 700 bytes .../Loupe.imageset/SearchBarIconLight@3x.png | Bin 0 -> 476 bytes .../Contents.json | 1 + .../ModernContactListBroadcastIcon@2x.png | Bin 1308 -> 981 bytes .../ModernContactListBroadcastIcon@3x.png | Bin 0 -> 1344 bytes .../Contents.json | 1 + .../ModernContactListCreateGroupIcon@2x.png | Bin 1289 -> 1630 bytes .../ModernContactListCreateGroupIcon@3x.png | Bin 0 -> 2243 bytes .../Contents.json | 1 + ...dernContactListCreateSecretChatIcon@2x.png | Bin 1376 -> 1557 bytes ...dernContactListCreateSecretChatIcon@3x.png | Bin 0 -> 2008 bytes Images.xcassets/Item List/Contents.json | 9 + .../ChannelVerifiedIconMedium@2x.png | Bin 0 -> 448 bytes .../ChannelVerifiedIconMedium@3x.png | Bin 0 -> 596 bytes .../PeerVerifiedIcon.imageset/Contents.json | 22 + .../Empty List Placeholders/Contents.json | 9 + .../Files.imageset/Contents.json | 21 + .../SharedMediaEmptyFilesIcon@2x.png | Bin 0 -> 8892 bytes .../ImagesAndVideo.imageset/Contents.json | 21 + .../SharedMediaEmptyIcon@2x.png | Bin 0 -> 2296 bytes .../Links.imageset/Contents.json | 22 + .../SharedMediaEmptyLinks@2x.png | Bin 0 -> 1823 bytes .../SharedMediaEmptyLinks@3x.png | Bin 0 -> 2774 bytes .../Music.imageset/Contents.json | 22 + .../SharedMediaEmptyMusicIcon@2x.png | Bin 0 -> 1112 bytes .../SharedMediaEmptyMusicIcon@3x.png | Bin 0 -> 1731 bytes .../SharedMediaNavigationBarArrow@2x.png | Bin 128 -> 0 bytes .../SharedMediaNavigationBarArrow@3x.png | Bin 378 -> 0 bytes Images.xcassets/Share/Contents.json | 9 + .../Contents.json | 22 + .../SearchBarIconLightLarge@2x.png | Bin 0 -> 918 bytes .../SearchBarIconLightLarge@3x.png | Bin 0 -> 1397 bytes .../Share/SearchIcon.imageset/Contents.json | 22 + .../ShareSearchIcon@2x.png | Bin 0 -> 1123 bytes .../ShareSearchIcon@3x.png | Bin 0 -> 758 bytes .../Share/ShareIcon.imageset/Contents.json | 22 + .../ShareExternalIcon@2x.png | Bin 0 -> 440 bytes .../ShareExternalIcon@3x.png | Bin 0 -> 558 bytes TelegramUI.xcodeproj/project.pbxproj | 114 +++- TelegramUI/ActivityIndicator.swift | 2 +- TelegramUI/AvatarGalleryController.swift | 111 ++-- TelegramUI/AvatarNode.swift | 2 +- TelegramUI/CallListController.swift | 1 + TelegramUI/ChannelInfoController.swift | 68 +- .../ChannelMembersSearchContainerNode.swift | 6 +- .../ChannelMembersSearchControllerNode.swift | 2 +- TelegramUI/ChatAvatarNavigationNode.swift | 4 +- TelegramUI/ChatBubbleVideoDecoration.swift | 66 ++ TelegramUI/ChatController.swift | 300 ++++++--- TelegramUI/ChatControllerInteraction.swift | 4 +- TelegramUI/ChatControllerNode.swift | 87 ++- TelegramUI/ChatEmptyItem.swift | 42 +- TelegramUI/ChatHistoryEntriesForView.swift | 21 +- TelegramUI/ChatHistoryEntry.swift | 145 +++-- TelegramUI/ChatHistoryGridNode.swift | 48 +- TelegramUI/ChatHistoryListNode.swift | 111 +++- TelegramUI/ChatHistoryNavigationButtons.swift | 9 + TelegramUI/ChatHistoryNode.swift | 9 + .../ChatHistorySearchContainerNode.swift | 304 +++++++++ TelegramUI/ChatHistoryViewForLocation.swift | 93 ++- TelegramUI/ChatImageGalleryItem.swift | 4 +- TelegramUI/ChatInfoTitlePanelNode.swift | 9 +- TelegramUI/ChatInterfaceInputContexts.swift | 8 +- TelegramUI/ChatInterfaceInputNodes.swift | 5 +- TelegramUI/ChatInterfaceState.swift | 6 +- .../ChatInterfaceStateContextMenus.swift | 16 +- .../ChatInterfaceStateContextQueries.swift | 16 +- .../ChatInterfaceStateInputPanels.swift | 22 +- .../ChatInterfaceStateNavigationButtons.swift | 10 +- TelegramUI/ChatInterfaceTitlePanelNodes.swift | 2 +- .../ChatItemGalleryFooterContentNode.swift | 18 +- TelegramUI/ChatListController.swift | 9 +- TelegramUI/ChatListItem.swift | 309 ++++----- TelegramUI/ChatListItemStrings.swift | 134 ++++ TelegramUI/ChatListNode.swift | 200 +++++- TelegramUI/ChatListNodeEntries.swift | 29 +- TelegramUI/ChatListRecentPeersListItem.swift | 6 + TelegramUI/ChatListSearchContainerNode.swift | 128 +++- TelegramUI/ChatListSearchItem.swift | 4 +- TelegramUI/ChatListSearchItemHeader.swift | 22 +- .../ChatListSearchRecentPeersNode.swift | 79 ++- TelegramUI/ChatListTypingNode.swift | 150 +++++ TelegramUI/ChatLoadingNode.swift | 35 ++ TelegramUI/ChatMediaInputGifPane.swift | 104 +-- TelegramUI/ChatMediaInputNode.swift | 49 +- TelegramUI/ChatMessageActionItemNode.swift | 591 +++++++++--------- .../ChatMessageAttachedContentNode.swift | 91 ++- TelegramUI/ChatMessageBubbleItemNode.swift | 26 +- TelegramUI/ChatMessageDateHeader.swift | 26 +- .../ChatMessageInstantVideoItemNode.swift | 6 +- .../ChatMessageInteractiveMediaNode.swift | 29 +- TelegramUI/ChatMessageItemView.swift | 2 +- TelegramUI/ChatMessageStickerItemNode.swift | 23 + .../ChatMessageWebpageBubbleContentNode.swift | 16 +- .../ChatPinnedMessageTitlePanelNode.swift | 55 +- .../ChatPresentationInterfaceState.swift | 59 +- TelegramUI/ChatTextInputAccessoryItem.swift | 37 ++ TelegramUI/ChatTextInputPanelNode.swift | 82 +-- ...rs.swift => ChatTextInputPanelState.swift} | 105 ++-- TelegramUI/ChatTitleView.swift | 25 +- TelegramUI/ChatVideoGalleryItem.swift | 2 +- .../CommandChatInputContextPanelNode.swift | 2 +- TelegramUI/ComposeController.swift | 2 +- TelegramUI/ContactListNode.swift | 2 +- .../ContactMultiselectionController.swift | 79 ++- TelegramUI/ContactSelectionController.swift | 2 +- TelegramUI/ContactsController.swift | 2 +- TelegramUI/ContactsControllerNode.swift | 13 +- TelegramUI/ContactsPeerItem.swift | 143 ++++- TelegramUI/ContactsSearchContainerNode.swift | 47 +- .../DataAndStorageSettingsController.swift | 42 +- TelegramUI/DeclareEncodables.swift | 1 + TelegramUI/DefaultDarkPresentationTheme.swift | 4 + TelegramUI/DefaultPresentationTheme.swift | 12 +- TelegramUI/EmbedGalleryVideoItem.swift | 2 +- TelegramUI/FetchManager.swift | 159 +++++ TelegramUI/FetchManagerLocation.swift | 101 +++ TelegramUI/GalleryController.swift | 11 +- TelegramUI/GalleryPagerNode.swift | 121 +++- TelegramUI/GroupInfoController.swift | 122 +++- .../HashtagChatInputContextPanelNode.swift | 80 ++- TelegramUI/HashtagChatInputPanelItem.swift | 4 +- TelegramUI/HorizontalPeerItem.swift | 87 ++- TelegramUI/InAppNotificationSettings.swift | 18 +- TelegramUI/InstantImageGalleryItem.swift | 4 +- TelegramUI/InstantPageController.swift | 8 +- TelegramUI/InstantPageControllerNode.swift | 45 +- TelegramUI/InstantPageLayoutSpacings.swift | 2 +- TelegramUI/ItemListAvatarAndNameItem.swift | 83 ++- TelegramUI/ItemListControllerNode.swift | 2 +- TelegramUI/ItemListStickerPackItem.swift | 2 +- TelegramUI/LegacyCamera.swift | 3 + TelegramUI/LinkHighlightingNode.swift | 4 +- TelegramUI/ListSectionHeaderNode.swift | 36 ++ ...MediaNavigationAccessoryItemListNode.swift | 2 +- .../MentionChatInputContextPanelNode.swift | 2 +- TelegramUI/MessageContentKind.swift | 107 ++++ TelegramUI/NetworkStatusTitleView.swift | 2 +- TelegramUI/NetworkUsageStatsController.swift | 87 +-- TelegramUI/NotificationSoundSelection.swift | 130 +++- TelegramUI/NotificationsAndSounds.swift | 37 +- TelegramUI/NumericFormat.swift | 6 +- TelegramUI/OverlayUniversalVideoNode.swift | 18 +- TelegramUI/OverlayVideoDecoration.swift | 119 +++- TelegramUI/PeerAvatarImageGalleryItem.swift | 20 +- TelegramUI/PeerMediaAudioPlaylist.swift | 2 +- .../PeerMediaCollectionController.swift | 103 ++- .../PeerMediaCollectionControllerNode.swift | 279 ++++++++- TelegramUI/PeerMediaCollectionEmptyNode.swift | 97 +++ .../PeerMediaCollectionInterfaceState.swift | 4 +- .../PeerMediaCollectionSectionsNode.swift | 4 +- TelegramUI/PeerMediaCollectionTitleView.swift | 95 --- TelegramUI/PeerMessagesMediaPlaylist.swift | 2 +- TelegramUI/PeerNotificationSoundStrings.swift | 26 +- TelegramUI/PeerSelectionController.swift | 2 +- TelegramUI/PhotoResources.swift | 112 +++- TelegramUI/PreferencesKeys.swift | 2 +- .../PreparedChatHistoryViewTransition.swift | 4 +- TelegramUI/PresentationResourceKey.swift | 11 +- TelegramUI/PresentationResourcesChat.swift | 16 +- .../PresentationResourcesChatList.swift | 18 + .../PresentationResourcesItemList.swift | 20 +- .../PresentationResourcesRootController.swift | 26 +- TelegramUI/PresentationStrings.swift | 2 + TelegramUI/PresentationTheme.swift | 18 +- TelegramUI/PrivacyAndSecurityController.swift | 74 +-- TelegramUI/SaveToCameraRoll.swift | 67 ++ TelegramUI/SearchBarNode.swift | 150 ++++- TelegramUI/SearchBarPlaceholderNode.swift | 41 +- TelegramUI/SearchDisplayController.swift | 22 +- .../SearchDisplayControllerContentNode.swift | 3 + TelegramUI/SelectablePeerNode.swift | 148 +++++ TelegramUI/ServiceSoundManager.swift | 6 + TelegramUI/ShareActionButtonNode.swift | 14 +- TelegramUI/ShareContentContainerNode.swift | 13 + TelegramUI/ShareController.swift | 109 +++- TelegramUI/ShareControllerNode.swift | 523 +++++++++------- TelegramUI/ShareControllerPeerGridItem.swift | 188 +++--- .../ShareControllerRecentPeersGridItem.swift | 84 +++ TelegramUI/ShareInputFieldNode.swift | 133 ++++ TelegramUI/ShareLoadingContainerNode.swift | 43 ++ TelegramUI/SharePeersContainerNode.swift | 265 ++++++++ TelegramUI/ShareSearchBarNode.swift | 102 +++ TelegramUI/ShareSearchContainerNode.swift | 570 +++++++++++++++++ TelegramUI/StickerPackPreviewController.swift | 2 +- .../StickerPackPreviewControllerNode.swift | 1 + TelegramUI/StorageUsageController.swift | 70 +-- TelegramUI/StoredMessageFromSearchPeer.swift | 18 + TelegramUI/TelegramApplicationContext.swift | 15 +- .../TelegramInitializeLegacyComponents.swift | 7 +- TelegramUI/TextNode.swift | 16 +- TelegramUI/ThemeGalleryItem.swift | 4 +- TelegramUI/ThemeGridController.swift | 4 + TelegramUI/ThemeGridControllerNode.swift | 4 + TelegramUI/TransformImageArguments.swift | 29 + TelegramUI/TransformImageNode.swift | 89 ++- TelegramUI/UniversalVideoCalleryItem.swift | 61 +- TelegramUI/UniversalVideoContentManager.swift | 14 +- TelegramUI/UniversalVideoNode.swift | 2 +- TelegramUI/UrlHandling.swift | 14 +- TelegramUI/UserInfoController.swift | 172 +++-- ...textResultsChatInputContextPanelNode.swift | 2 +- TelegramUI/WebEmbedVideoContent.swift | 215 +++++++ .../ZoomableContentGalleryItemNode.swift | 58 +- 215 files changed, 8023 insertions(+), 2327 deletions(-) create mode 100644 Images.xcassets/Chat List/ComposeIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@2x.png create mode 100644 Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@3x.png create mode 100644 Images.xcassets/Chat List/PeerVerifiedIcon.imageset/ChannelVerifiedIconSmall@2x.png create mode 100644 Images.xcassets/Chat List/PeerVerifiedIcon.imageset/ChannelVerifiedIconSmall@3x.png rename Images.xcassets/{Media Grid/TitleViewModeSelectionArrow.imageset => Chat List/PeerVerifiedIcon.imageset}/Contents.json (69%) create mode 100644 Images.xcassets/Components/Contents.json create mode 100644 Images.xcassets/Components/Search Bar/Clear.imageset/Clear.pdf create mode 100644 Images.xcassets/Components/Search Bar/Clear.imageset/Contents.json create mode 100644 Images.xcassets/Components/Search Bar/Contents.json create mode 100644 Images.xcassets/Components/Search Bar/Loupe.imageset/Contents.json create mode 100644 Images.xcassets/Components/Search Bar/Loupe.imageset/SearchBarIconLight@2x.png create mode 100644 Images.xcassets/Components/Search Bar/Loupe.imageset/SearchBarIconLight@3x.png create mode 100644 Images.xcassets/Contact List/CreateChannelActionIcon.imageset/ModernContactListBroadcastIcon@3x.png create mode 100644 Images.xcassets/Contact List/CreateGroupActionIcon.imageset/ModernContactListCreateGroupIcon@3x.png create mode 100644 Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/ModernContactListCreateSecretChatIcon@3x.png create mode 100644 Images.xcassets/Item List/Contents.json create mode 100644 Images.xcassets/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@2x.png create mode 100644 Images.xcassets/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@3x.png create mode 100644 Images.xcassets/Item List/PeerVerifiedIcon.imageset/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/SharedMediaEmptyFilesIcon@2x.png create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/SharedMediaEmptyIcon@2x.png create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Links.imageset/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Links.imageset/SharedMediaEmptyLinks@2x.png create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Links.imageset/SharedMediaEmptyLinks@3x.png create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/Contents.json create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/SharedMediaEmptyMusicIcon@2x.png create mode 100644 Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/SharedMediaEmptyMusicIcon@3x.png delete mode 100644 Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/SharedMediaNavigationBarArrow@2x.png delete mode 100644 Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/SharedMediaNavigationBarArrow@3x.png create mode 100644 Images.xcassets/Share/Contents.json create mode 100644 Images.xcassets/Share/SearchBarSearchIcon.imageset/Contents.json create mode 100644 Images.xcassets/Share/SearchBarSearchIcon.imageset/SearchBarIconLightLarge@2x.png create mode 100644 Images.xcassets/Share/SearchBarSearchIcon.imageset/SearchBarIconLightLarge@3x.png create mode 100644 Images.xcassets/Share/SearchIcon.imageset/Contents.json create mode 100644 Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@2x.png create mode 100644 Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@3x.png create mode 100644 Images.xcassets/Share/ShareIcon.imageset/Contents.json create mode 100644 Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@2x.png create mode 100644 Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@3x.png create mode 100644 TelegramUI/ChatBubbleVideoDecoration.swift create mode 100644 TelegramUI/ChatHistorySearchContainerNode.swift create mode 100644 TelegramUI/ChatListItemStrings.swift create mode 100644 TelegramUI/ChatListTypingNode.swift create mode 100644 TelegramUI/ChatLoadingNode.swift create mode 100644 TelegramUI/ChatTextInputAccessoryItem.swift rename TelegramUI/{ChatTextInputPanelNodeOperators.swift => ChatTextInputPanelState.swift} (50%) create mode 100644 TelegramUI/FetchManager.swift create mode 100644 TelegramUI/FetchManagerLocation.swift create mode 100644 TelegramUI/MessageContentKind.swift create mode 100644 TelegramUI/PeerMediaCollectionEmptyNode.swift delete mode 100644 TelegramUI/PeerMediaCollectionTitleView.swift create mode 100644 TelegramUI/SaveToCameraRoll.swift create mode 100644 TelegramUI/SelectablePeerNode.swift create mode 100644 TelegramUI/ShareContentContainerNode.swift create mode 100644 TelegramUI/ShareControllerRecentPeersGridItem.swift create mode 100644 TelegramUI/ShareInputFieldNode.swift create mode 100644 TelegramUI/ShareLoadingContainerNode.swift create mode 100644 TelegramUI/SharePeersContainerNode.swift create mode 100644 TelegramUI/ShareSearchBarNode.swift create mode 100644 TelegramUI/ShareSearchContainerNode.swift create mode 100644 TelegramUI/TransformImageArguments.swift create mode 100644 TelegramUI/WebEmbedVideoContent.swift diff --git a/Images.xcassets/Chat List/ComposeIcon.imageset/Contents.json b/Images.xcassets/Chat List/ComposeIcon.imageset/Contents.json new file mode 100644 index 0000000000..1320d1b0c8 --- /dev/null +++ b/Images.xcassets/Chat List/ComposeIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernNavigationComposeButtonIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ModernNavigationComposeButtonIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@2x.png b/Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b280e9c8b52fce99688518ba65bbaad8a7fa8bc GIT binary patch literal 503 zcmVtCvu5id4<9OP&jv-qfu!zdbyal7eYy4-0TWo zj=9zB%q8=Z*G%=>$v>I>gZCr#C?GZ3`v4KGf{e#|0X+;zRS-)5*xTe|yq=Zk2|&6@ zzK}b&$HR6HjHo&_z84NS7&^nt<^%lyyJDBOSoH z9`p^}U}!T@JP)X@O>C0POFh&p=*Q$dq9U*nO6R}y4v|{^Mym@e8r5bJ88$wlWozbF zwA2I4%-~L5L{+z<1(iA44)-7J1a8nmom83CIDS1KI^VjmL=}(ef}YYd zb;Qt);k+yIc2i4lnxp-L%XsdOigwJ=zQIMj#JVKXeS1qB?dy#fXb~bcn>_P;{DoU4 tzymzM13bV3Jir4y!1Lda;l{(>;Q+(ZqjKA*R}TOH002ovPDHLkV1kOy;FACV literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@3x.png b/Images.xcassets/Chat List/ComposeIcon.imageset/ModernNavigationComposeButtonIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b13c6ecaa073e5d5af784ab91324914723764a GIT binary patch literal 701 zcmeAS@N?(olHy`uVBq!ia0vp^t{}|81|(&AwKW+Sm|8qt978G?-`?7KDa28L^}#XO zRRx^|CptH5ZxA_qp{9zvT>ZFg97B0|xyQ`7!$vuE_t&NN&ODmuKYx{QsmcA)Mvcu{ z#w-)!m~Qb%++y`M7FKw*=AR8iPwl0M<4ffyEaEM=Qrf|v^78n;gcpp-e?pe13NA`H z#&BtOmqy#ZgF#Y)CoNmp4hCHpoYi#mV`$=pYp2;GXUej*#W7ym7~kah+k{Ub?t@~% zy4X?)D@|#Zm#WPd!VO+#Zn#^zlB2nVaoVnTLH;Y8Pd9B@%9*w@JD#PvM0DD+>SJt& zJZ~PY>C4=!wf5}yiQGvqTy+KJ8TZ_XF4v#1OlfY0%S$fROewXs9^r|x%fbaYE*0ww zIIByl?9|}dWODtte?!3}Aj9?s&_TJjhZPt#xPrQrm|Lo+%};%@z@THJ{~^aE3w8e5T##mYYsogT`H(_}{B_n%S4yWe z3h@=~V_|tKD5&8(U$M!-Y=@kxg70RR#UILNy4NX`EEH|I%ob$d#Uc^!1yDc}{mVB4l&!TT8|oaRwtZ*`pTuD#jAiIaWj zA^laV3qSl<{C>zWW#JTd&!x-5*&q5#P707`F~&)Y&dkk^7Q}kZ`)W}{__j=C0z>kcJQihc1&{cyt(tC zLQ-HxtR_n>LvlXjt+_lxqFz;X$>FADY04LtJTw2ID%d3?=Nc%24++M*22QbWy2Owd Xe(2t54l5U6vSjdd^>bP0l+XkKoI4t5 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat List/PeerVerifiedIcon.imageset/ChannelVerifiedIconSmall@2x.png b/Images.xcassets/Chat List/PeerVerifiedIcon.imageset/ChannelVerifiedIconSmall@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2867f5033f31789f76962ec02d2576f51b430f GIT binary patch literal 538 zcmV+#0_FXQP)@_?s0L zYz>>@tE0Fe{U!+{(t$_c(ss}dM)W$ZP6|1vzuQ_WeGlC%lGqxy$yC<+$ZNae`X_G) zjBXZ*%WzaC$mgGbEwtg*XD&9 zr{8Q$&JY;gV6Z^x!70O3m^UOc7>rAO7WdP){ZACR^x3(G8LET2Rji?RlHmJhxY;D_ZbQ{!eclO!Fc21Igyx!TTKRFjO z^PltO-ZiN(r7*scGCqIk_4r04gu;6Cq2Cp-0XpwSHJ(E?!xAA`;{A@Q8_Nb?G@E-{ zk8#Xn9oC}{{qT_vCyaFj6cqwR_!G*^m8u@u2k-cfaUbh-xxd=^h+dTOT$no*eBdW< zTholoFqS3wN!r#73Pag~uZ%tPh8uA3bp)pBd<2Z#68uAk8hD9!R2V-gl;tKI{E>sa zz}we_n6_f^ajNl5G?$c+hg{?rgQ6FrT8YU+lo{z*Tv|dtI8;5drM|b)0h04v% z4GIuE;Ig;7vu=Z-f&~Om!7GlLB>{%Y5pcU+#|7x;Gl^Z}@0fM4hm-{{aDaytEXs^a z!32-K$Xx@2z~SX$(p(a#@u(-!icZV6X55T)1q=d*7dYcUATYaZK-pjrIEuqD_G>BvcPPC3ulKt_~HOiIv4~l{G|`^#Q~snFbG^^s77 zPekEEWY=!Xsw{%ui-4!9Zmv=<$Fw6s6xLxq`p^#_;zr@`D9Qf+WDK^WfItIM00000 LNkvXXu0mjfg4=9P literal 0 HcmV?d00001 diff --git a/Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/Contents.json b/Images.xcassets/Chat List/PeerVerifiedIcon.imageset/Contents.json similarity index 69% rename from Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/Contents.json rename to Images.xcassets/Chat List/PeerVerifiedIcon.imageset/Contents.json index 3095d79e21..1d988ca1c1 100644 --- a/Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/Contents.json +++ b/Images.xcassets/Chat List/PeerVerifiedIcon.imageset/Contents.json @@ -6,12 +6,12 @@ }, { "idiom" : "universal", - "filename" : "SharedMediaNavigationBarArrow@2x.png", + "filename" : "ChannelVerifiedIconSmall@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "SharedMediaNavigationBarArrow@3x.png", + "filename" : "ChannelVerifiedIconSmall@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Components/Contents.json b/Images.xcassets/Components/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Components/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/Components/Search Bar/Clear.imageset/Clear.pdf b/Images.xcassets/Components/Search Bar/Clear.imageset/Clear.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f597a23849298ea7286986842f3d1fd68cb09cde GIT binary patch literal 4177 zcmai%c|26@`^PO)7_yXzRFfFVHajZoV2HA%j10!i*tf=7k|j&F>;{GGk*z4AA(ACM zCfQOU*+bTl;qjem`91Y}zOUE!yw2;K`+UxIU-x~^_4(s{Ng8SC$U)`dK*^Sg<%!v% zmAkK7T7d`<1mte(1U!8jq+mdDv-ffUAsLbhNI}QZ#fwB{++FZqBrOubok#+ysscT| z$RxZg(2o^oq8~M+&hvpTNaZ!alr{EyhG73J2=1s?%kNd#NNC#KSzuk9AGd^3 z)!`9sY|Tw~GuyzPB+47-I-F`9%$E7+Sk0HU0#sX!*s?F~D(KusIQXix#U&)QT$o|OtfmtUer)??`F9$B8{ zy_a2x8X5;uJG$G|tbn{0a>DIWPZo1@Whfum$+8jA$t2`*JnK!_b*r6%R1M3s)l^(b zZttu7lV@YDyb#_MOTrQcVIue%)t+?MR~?{z`TWC+RjB@6o~g z{!$$=+qV&VNYU~mowDTrdUeV3S^yOte(K!gY`U^lsr3ieVYQLg^`$joC^q*7kmN@E zSs=!USyyH;nRf&P#eB&e!hS3H`70hIkb)83{`-|oa`OVgf0W;Z8_ z-OY>f-V?;M48tF1Nkm7yrn?{L5`-Z@5sVWDWEkYY&>1B@f}f{fV0L%}{;yL1Ev zq%RQ9KV!v2h!eC0xo?En4sb2=8?`nWYiUmm#W-^Djnr zzyc9Ww|+GR&T)x^X$y*ciCku5;Y@B(eoE$z{-*4Z#o||JKOO~$)Uz!V7)W!Rm~OY* zT&gntW($=`etvfJrk??AwVLM^L^!CzJyn}k66>1okT72+Hb0N;pLF=vI5=|ny6Lt` z`&GKx5_pc5r4f^b9dVf}RiFy7tVvkhK2+6^V#ZSKx?6o>wA63`#In4LrF!_z%HePq ztHhR1JW|}zb8{}9tl7@;GSiCm>=5^tjp54c>78Hb7fuxls|6p^+YcvcPZSmv>TA7v z#luUX5x3X*cgCJ6JO0)2T=q`(GvTGMX*1*LWUkOihx)M7!RGPuL||xO-)P_gloW@^ z^-O0$2XAYZfG}mFT0M?-cNG@&F?IvNn6fg5VzP!yuN>AYFd;RCak? zHb-^9Lk-opEILI_`z=c4VkqYc17 zvB1M4aYtGkrQ*3P0L~$NM^ix(E)wVYMLDN9w?hS*5yrvy486p|`LDEC+DbpT^;YAl zo*4LClAvC=gs_Q(bb590c|9j_99RWRJ(|>Ul87N6z^YprmP=%VtR?vp)r^vlg9Q!G zh-qsMo>M;aME#2T6?B2go_lV+<|S!$nHGjrt$;HD3Ya7~WrZ@Q+!8v7?;xB2K zJV5cKSj%TiSsx8N^LOD^Et_$ZdXs5zUgd=xozd!Ml`uW+jNBC4o3RrPo-17op3vGEk!)VE`>EE zuEfC1y-%<&v&6N;uovP4cEURiI??;&@5j=LXuesrOl!o>_*TT{%DTv_wVO^~pMO^Q zoFtkd+8`<-Y9>lSv?4_^+%h^cCNicFZPgAI+>Zbj6BbjCtRN}rUtXQJldOtX&M7OO zFOut4%^$k2QbxE}Wp1B$uBvMvE>ZrHqDGBnjj6Aup}vB-f^~)QLXN6&c3F0QBc}0E z;LB&PLGNs$HEuWE%ZbWq#Ao62MhVAC67F}O$g{{h)v82zd^u`Kb?g$jGBQ&yQ7^XR ztm)g{;qx|iv}ziU^P~e9aUWUVmRtD&i!IgZzpMQGbo;fuE4RPNaH~X{M`sER30cd| z%bLo~${xW=V|SUmR)4PCeEzN_!x={iXbX25BUE)24oyBPE0!DaX`Bx@m4T}q5~@E} zPg~<%6Q_geP!5#{zX&St`dcrb+ju^5FJw zdLEFMMU;Z)uGE_OUH9_^$k}RZp9@5qlPiaTlL@zzb)%O?&W zJaTA+eRk3M=ov|bJ#FHVk6&Xa~0cNhQV7+Sqi-J>aqkE)cx!BwQF3eORF5Om#3$G$IS z`*PL06AX@fV(seFZvAcWRX0ZBw&V>gca_t_VS5ZI`=#+KfyveJjB&TlLmev}^y^4F zq_4MqgU{dLTTRF>&b(z|{pFVLpPci>HPwxNZJ3R71-a@DE_Uj4#*z+>xJ3>38s9ap zY_M^tT9|Cre4=SQN|o!n#Ow0HWxvb(kX==o(|d}zRrgoh&QDRF3?)A#&W>OME`A$& zwH7*0Em$j{`+oG{^!+e2Q@3KCZi}@Vd|mz#S2po#Id{3H{^DH21^lJUHhhk8#JFnM zy@%8@6;7DGqR;uCgGJ*qCaItOYvwcGwY?u`9B~coVU1;f!J&FsQ0!@7`?`^HnTG2N z{D;xf=d{_}&vlLVf!2Xq^SVn3G)x$#pshL1yYE!*fZ{#HWK?r(V(Wvk%Wdx4AHSmg z_uZc{g6BQi04N<*Ui#FwabzKj=k(x}sDP3gsqO5w$FD7-EOPok^hZ9+Z%_>as4 z^<*CEIwYrdd*|9}%vyRz)k}MQT0+PF&-!h}zw=Y-5oeIbdpw5SH$Mzqt&q}qPyY4viXu*{_<59JpI#zb8 zVOPM^CXTK!yjB-5acd{@Aba%g%`fr|{ySD5RAvH(x4Sut`K;7lT~*q4*e1-4P@SLf~y*wCtO+Q)_0q7`;hzEv1L-$g5?$>>z;e8!YvXffYnhORzX zeX-aXL+Kd{Yn1-`@p?-4{p{<$bgI2=N6Vsf_iVb_nc;g4%A5Ubu2aP6@g85tg~Fn! z;rk70Yr)$&bi!&|!pxlEhG9x}teWpNuB~N1u&dqV$a?4s{ohp0q|omuje^2{!77tq znHmFf_0VWdyeEkWVgjrQ2=^<5iPpcE_#g82{O>Hygk*09k3&HU`i?|T20SytossP~ zK!!5W`_GJMycgcZ-TnvEd;X;Pe~=mq{W;_KSv&z`XbLh1K_T)`m^=z(?2RXTT?H`+ z{X6RWc}f4;H}qG_a9ryo3<6bzC_@o22n-2DSwJ9CjPsw!-}b|xexf&l(L#P-lhHAt z8J(k-qr00H6WPD#WVR9R#NVI)*{L6yWCvt47H}Zs{|*oeiG(9TcA%dc9Hqo)aF_?k z?Ux2Y!WiH9rv^tL872EugFs-6&-#BF67p{i1!L6h&p0>&#^{3n)F3G3|FSORUvY3G zf>E_U=faW7|JG25-`4da;~iZ{jChqPGe=t43_>p>UcJ2(b{1|I7 z>~dNOjv(M|?TADKLKy~CB*36hA`C%5;h`u+B1ws$3jE(8KkVk|#jwWrg1{9~5TK-_ IwxJI2KkCpPYybcN literal 0 HcmV?d00001 diff --git a/Images.xcassets/Components/Search Bar/Clear.imageset/Contents.json b/Images.xcassets/Components/Search Bar/Clear.imageset/Contents.json new file mode 100644 index 0000000000..0ba85044f4 --- /dev/null +++ b/Images.xcassets/Components/Search Bar/Clear.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Clear.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Components/Search Bar/Contents.json b/Images.xcassets/Components/Search Bar/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Components/Search Bar/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/Components/Search Bar/Loupe.imageset/Contents.json b/Images.xcassets/Components/Search Bar/Loupe.imageset/Contents.json new file mode 100644 index 0000000000..426f9681b7 --- /dev/null +++ b/Images.xcassets/Components/Search Bar/Loupe.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SearchBarIconLight@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SearchBarIconLight@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Components/Search Bar/Loupe.imageset/SearchBarIconLight@2x.png b/Images.xcassets/Components/Search Bar/Loupe.imageset/SearchBarIconLight@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..846732f3b3b1391d55779a2c58e3541faffa9f7b GIT binary patch literal 700 zcmV;t0z>_YP)Px%c1c7*R7ef2l|N70Kp4h-=McouFVLY&$E>WCP=|_kqWug=5rqmIiAtSHx2i}n zs6i-FJ^@3^hUieSRAW`h=`?Gziaqb*PiY4dn-uHQ)_k4ezsKD_Ihr@-N zo7=TCRcn%7Rw{T1%$VS}G*?=i)1{^MX}8<`OT5XWs-VNe!OJu?`y%?dnk$c`_)o3X z?)LW9*K*v~*EV5{(f$75Q<9i36j4fD%Nw;?3!TM9{eYJ~F>+wQ!hvgbeC_y)AjsnE z3h-8hdOJIt$I_-QBEkm;M;p@iCEpGLwcqP)y>(%2EGxuvF*^xLk=FAl+W2k(b!{9U zKQ-p=Ofc;*RIlomQM2*v34(k{(<0!(!l91_TJ&9y=5*OTVYytOx>&Kq*`A^0!|LJ) zlOOr^#78Dxa7Oe?S`6cR8tZ`usQV1Pv#U`4zg%#m}mr?ZEPI`i_v3gvX4BmyUOrSi0+=4Q{3$za2d0<&M zJ0-bCDQu{Xcg68wvu+v5<(S;(P!bDzk|}zA#_pYyk3JR-&W*no&WOus?+K@?*m(A{ zQ`M9$_z*>J_NsBl<3cvs{EtsvvRr(LT#@&d?<+P@2!iLYoMo1K!VwNn${QcYpZBH} za~86*o28vrESyEG4tsuncCotpf{CvbxE0348PBZ{Vs*vlwsaJ-k82sTdt?FgYB}PZ izIo|Q)#1x{0saG>Su)8z<8?{^0000LAR)3?z5gE@TH%`2jv5u0Z<#|Nj#wPVMWP(%(M? zLe8H*zrPw}|RtTlX-0voZJtZ=`#>(Dr<3&S{glQ~XYKB5eP-b-FAy`c$+$88_|GSiMT;6OV##$Qrg; z+kStGz0I(dX_sc;vR;9R^I;aFJ&uUe6{tTYboSds^=Oo_w%!lAr7VaCfB!ns21FI uzPxA6(dsJIfUEb}LjBl6ec0}A_|KdnAw8MX<1H`<7(8A5T-G@yGyworIoZkp literal 0 HcmV?d00001 diff --git a/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/Contents.json b/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/Contents.json index 160d2fbd89..a41eba8f54 100644 --- a/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/Contents.json +++ b/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "ModernContactListBroadcastIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/ModernContactListBroadcastIcon@2x.png b/Images.xcassets/Contact List/CreateChannelActionIcon.imageset/ModernContactListBroadcastIcon@2x.png index 4069a53db0e9039040550bf857a5a777be6fa914..d62df72428ebe91e4a279eda8f094571d04e02fa 100644 GIT binary patch literal 981 zcmV;`11kK9P)oVfKuZ9V11P0;R2s+Ky3z)EZ{tU z&@3F^(XWm-O7DL%pgO}yW_3P3RS_)2=dykcEr_0c)d?=6rlDMczlQK}H&-x`04LrgOx*Ys582UVMEp$pE9& z6cFRx(nXg|-S)vjB=Z{u0#2Q=$I4t8@y^ckEjDnTQj;$-&pS=D83ZJf>9s5NtC!_m z9dBiQet-*n^Z3W#6Ghy=8O38P$(%;%O#%)i8RUqsvYx-vMz`lf6XeY2hfh4#^&$b2 zc!7Uxn^64!_{VQH@Ru&cr)}U1tSc5#G9iFw?;r8pU~g>)yo>ViIg1!nrKl+BCU~Eh*xPl$tY6_lyd(S;$?Ml9a9dN2|DIE&o{I( z@?dAF>kUIS0ZSj2J|>kgt=Y|O{_WDmUe|(~(C+DC-&1!46YFIK^zjwlR)xgUjzmA= zjg#~HsHwstBr_+WdDLk#3l(X?)lp&lDS*49hUf7&kt41SDzo!itXCI$UN}RrYDot@n*$9ugP*Nw`=|1 z9Ei{B@fC&({Jvg{Rv6{CD~CEQ>bUf(KiXKz_fHY=SSz*zTDw}$zo^dOP)QY1frk-n7{!TYgbXXz%du|?5DY{S2qd)&nLMZLEs%= zYZpOo@DuNR-$&U*c(;7zlu5kgalD;Qk&VCA1$EIIV4N!-W+Q*3tM+>4+kYue*ld&S zbSnz7gBg$!McJ3qspKQ2Q`U6#x3UYiN7S~vl|&7LBw)47#fl?(TuiLxr>RY-6$JHy z2w?Ry*Z+stAz|&R<@`6>sWcYtR9yH?C_gwOzXC>c|W^vwmS$SPI{mZfz9Z;ZJdPc0e03*eqCtlY4_jF#>rQ| zp5Zk|!;76afnScBT0vkf=_g-4EkQ#d3fM)mdB1YCP6F@JD+~|DN4^5}qS*>yR*)`L zyM?}Z=>H|KphsA2zcNH^Vzv2Z(<)IQrJt8{DvTGK*MgoQ7Cr>vpn^k?b(s(;|)T$1btR7Y2>7B>4sF(e3kLt?uu6NVB(g6sxWM`KnC!~QFvbG}QrG}prU z*vXs4pV1Z&=9ff7k=+KuHbi0lt@Xy_!i>Bz8`!g0m^jClCAM*yuophh^EozL6N#(= z>pU?k&P#0X$b=yu@~G(PsEtNe@bZ_Z^4O%rc1k7;B|DzSPSqlLggm%tVF=UP;;ur& zE(0#fggyRlrbM<*tvbamuys+wuwpn8yP&b8nv;pbBD#w|06iPbZlUCyI!|$Bg*k=$ z25YXbH(E|54izuiCiL!}zvtPUBB&eOq>^;*LbApnXA4~`w0lSUFrH&L+b;ZcN@&qpn80}cF z$%3S~qNr*KTllfKXvX#US0|O=S)-lm5#HLk2~px^_BE~f=UvI|8FhV_ukzY`1$3Et z;?sbU6OVy*PYI`wexa^0U#}d#_IugTY0KhPiJ!KZ4?Z==sq;GQr@$c}|G-`wTxv_? zqhI88pN}!d>gJaqNWBeXZl~wDUsG+?)~&H21_=tPA&QqQn(x zyTVE`o>PJWaZ*^~(@Wkd^*lZ_HnY5wLxO>(?zn&@KGiz@L6ak9kfqc~q)6V&5y3#x z9p|!?9w@U@6Qcacx5ztM1_aj;Ho82H(@Am4=HA$5Db8)XcmwOwLk;nz#_=dkf_|(E z_cwNh#Eqhs$ZcAm$JV7EhXW{LvpthB;IOf_2iK<16($vRuO)DgwcJbrX`+o-7Bi zVwgi9Sj}X;T=PanQ4$2JJy~sI7ZprN5G)COx;g>`7mwgc^+6N_gJ2x8utdX5!XXQe z4wy-BRgN)72Z9Z7RU%x7U<1r2gbQIlA>a@52`M{Bn;oM-;=A?y)<{fzN9l^^k4IAJ z3X0D>pOeSpGn9`~Y)AQM;ei5hv~ciz8d^AH084?Cr-#aw@1!`P@%)~$wf8De6BZCW zMCcwRc_CT|FHjpL5Nv&TLeJisHbV0p&j8?s&WzkN!4k2+;H4|?w zZv_a0S{4n#>k#67(x!?y95Rso?=AgZcoHrBtAz1<@7nW+Pk0#aNC<>5H-CPzDzN*<{*O&4%~I&aFl^jT)|05^K-8DWXH&Oo+oNCKU4{F(ZoK9vvbKZpX7v!$K>S;6U7C~SS^6| zfjE_9T$G4v?2gN1Aq!*(OMJ3g{;tVNHbdYR6gjl~YS*R-#p$z<1u}$jeEsl#O5xb7 zWPywz!FGKwueb~=lJ)%MjK{|b>QF^M64-T2w#+=K7fZ4_YZ9o_vKI-$mlM#p#25ee zaqwn!s2(8sRo3%|D&BkceR9=2Fzi<%kY-ROCLnth6z8;+#w;xz42?Y5Nm9B{{h*m-FCp zq!eMAOQb}ukz2w_h2)Z3By4Nv`=;-C_wE0F|KIz2f4}$Jv+HCRdqo8m1r!RUNOG__ z0lvxbBQFE?>Q~Y^D3mms>~zu=qzEEbM#NY|q|6dxKrn?^L~N2F#vmf_jA07EjsqAL zco1M%0%b&mLxeD836-M-_^V=N0AmTT;1C280L0iEfD@zw$jKC`{|(9^Gx53@to2@? z$`a7RS{See0^4aKVvRV62cZ%J!K!RAIL9(Y5&+~;fqGysOf(dU128BX_JWNNfEN;C z#rRmruN?hTE#>~Y3&;R<#z;sIP$-i40?I*vSC9nk;E<3A8L-g+1HI!VB{)Q47MwFh zDxd`{f!^UjfVQh*RYVLV!St#aNCn~np|(U7NP$iqbR^V6*55+GunPcy5_k~;?PM`F z8jATjb1?Qecq6b8ONa$9$^5ZE4^yZn;RKHGk`|BwOcfD84U(&o3JKrd5wmf zPsIOsaXY_4Q7UR?X+!RUXm4tpJihY#zF_q8%cs|bI(Pamj99q4Tqxc$rm8XF)*z6% zG5e$E;JZxUq!=VUJmX5orZ2ASdXx8-J|BYA$fcaSD@20csFuRBk6Y(=WMk@NjlB-l zDt`)lEoYtI|E3pv>FsLDi#f9*l|P$~{1Nfc;n%NmxPUX)5}s6@oVL1U&_x<%=%M$&kvO^2aZ22Kg&PL=LPU9KrC#%@wTQVG0<;papF*w zP#3Prps`~l=VRXmPrEg==4UQ8hIMJ=oh44Zy>p>*c4?x}!m?y|X;P0QLcG~blM>cA zipAlEwsiECd^J%}(4BfPq9jIM{>WBBI~Z(iYoi>cr}M0)xVrLHdd`U0|p zT0_#ky7|KvE7BJ1%$TkL&2!OqvRji=OY~(MbNaIC+ivgwOY^2wwIkh|S7^u9=_IoU zKX22fB(iTCJ2c7L?d{aDYscA423O-7JZPgjRDxH&e{F*1*)d<~LhhdA;sep7k5TwS zr(0Q4u|Cee@gqJ)6zVl8hRMxIuCI|>i0{5r{b2>I9fa|=%;u;U87~jfLNm0?+;9Q* zW!^ijcbwb48JoUWy<mB`GcTQPf0l{ zJ)aF!u44$bjO)>)5ZU5v=IGw;(NJ|}&8j4%D5>mmY9QkgC!9OZJ>%u^>NIWIL(jdN ztEfu%A0k)Ka{LYXrKF}NMf&lBs?RvZ#yuXUj{W!O`K<$wUcA8mzl(WojceAP}B~e|MGY0HkX%7bTVlR#E642FhYp%}{ zZt*OPJKc6|RZ1ccQxjTq>Fv=GB&wzl}hcye6K(8O%F49~W0+g6;MZJRr1+qP}~eYWkp;EbJpbKfT4ODdh&8f@3R+2og`^;Ok- zJv}wkJu{j9&iSnqECjX&`+y@rK#pMD*6h>i)3kM9ZD4J1CU^x@L6njz$DB!xwlW4d zA2=KQ;5Z+GkHI70qVfME30U_C`-INeAE-HB83J4s+zD#J!B4pJ=yvwg~VMHg475cJ0kn8TApldW`>cTgc?>#kQU$pN1zx#w>l9J;Is zUB|}9?6Y*eupJ8zH|V?y_{4)l_k%uTUDx+V*k^}&;Q$sc);fD)8)IG9_eVPfxx<3?{rj^k)^T|KE+9@9L%<5^Nl z*z4+#X={t!>Xw9UiaRnmbGj>Gud6?J%U3C@CWvgBKQ*)0RbQnzFUPJ;5ZOM1Lp!Ki zM!IOfj{Sm0CrfJLb!Q`y-P1dSzS_?~7j3gJh8mc!QK!4~SNiflucN1xX1X#Px=TM! zBex{t7me35IP|)5Fv`2Gvn8=uu1zMsa(RqxSvX(a-O8!UF-tbsWV$Tvw&C|8X}%SF zk~0_G)zR((Yk|WQ-K=~Kh~*BFdMkn(<@2Wz`bMz!ESHnR#I<*o zY%`{k_W;#1IgGssTB&hnfkE=HM*ibB(=!@eOJpatf`Xg@hI|~Tnv&L_824O3K~4dq z21n#n4r2f1ShnU7&}(t%F-TaA=VIS;?N?VGdNe-9`eTgo&qSZkt%9WO4ZX*uBo&u9 z$HPAb%Y(U%0<6*I$V41ZTMXRgtghc11b7!%tSlc7oGYia2Y#aFd}aO|9}?}ZbdXq z&QQ^*+){2ul2n8~CDT-nQrM&Ytv&VqF>Al;UC;Zx&-<+X+mq?(;fhn&QO95~xIJ!8 zH1K4iKQ$$=F28ku41-aO@uYe=gRKq^(N0~&vt-&VSUVN0GVN3t&xY|VSQkR@AOoR! zG)R>ZQe`?2vMLRMAW#M4Ay|(GFpED50SP|sXUV!ugvds#@DM_PP<{~Wa1jSco`>XJ zKt2@)gvRF)JPMWzfDrGi2&xAa1A{6A0Sq#b0Z0N{Mv%8gC9zRFfrH>V$WPfU84+wj zFHjD_1PIpUBZgoK7EAy~s1g|oS^%=OA%p~!LyPMH8|VT6a*=vx0R{$a00{y-=nKXK z{HXwdjzHui#wej&8%y?soCxfaTc?LQ29R>W@{BeF%z;TD&qaWKv=F!r$#nw$A2R~M zkW7bz7^EY>VbC4GfLi%1C>z-RFh?E$VT<4tl*q3D#S`UHfkM)`ch)QC^Fh30?!1OkZv_T7r)D^ZEc7#K0C37rigw8vz99MCjFM z%e?~n(blLQw2M56N<#081;ZUx=quP4PxCm4(f{q*!?%lw^Mkdw+@|O1!^kAXAxJku z2ImsK@^E`zZHW0iybim!;L*|hD^(A4e6&+~`TF^7g@E@4`|zzHt_zkM-}L;methcO z?q4)J|JXv`%vrW7QH9&a_V{N$Mq2>!s4m6J&L{?VyrP<3hpNgzcy|aB5tDNH_ z-pc;3&i?C;2_MGdQ!>mBrmentSUvdOku(2^YVi7E?A^Znw<#e8qfFl8MDb$2_%I0j zt_;(A@Nf27SQP(QPtd-_B+J{~(wAyAFsnk0=elzl6->&ZI%@j>BRI;?II*{~L)_Za zFOAA8Ofo~tJycH=N9Zes3*zh|f|^nuT`cT9T|8+U=d!D~ck=W6Y4CU!u`t^d?ht@+ zNSS)^zQHYJa)C`8h)_{xe`#@K5G88FJgY+~Ypk*UMhvMMh8L|qM6}3mrN6C0dq&p17ox8B*m8D6Gx0B`X1p17;2{5$=zR>{uLg& zO7l<3|9N*Vf2xwOvS*A_*l&^X&_`?=7ZT)jp;y^jJxAQSri9rtVsh5K{M1-bpyY0R z!{KBHkIs>$zq89)b0#f0p8k#7?bJe^g&1(k(rvsEv%^GA*~*ASJY_l{AsK5vQ9;Jt zU%TrVrC{7tyHGQE^NLm62X7=xn6%8dnsjqzft{)KOlX0T@BS7oqdZEG(<{rg*Re*v zE#$_4zDmNd49Cnv&W_kJmmr+Y3%feA;V8a=?}@r6mE32FKxz?2HQT*d~{}ZW;_aD`ip|t*Ut(Lb0TLL*7V%9q52cq zIYPm_XMV=4f31I!vruYOhYNZdc3b_Wv|RLimvHxx!QVF71F>XWB zF}&j=iqb8m@B?cP20c|hD*nje54$&3_N0Zs|Lae+f$lo>jtw?5QWxTCf>Ftd8-l%c z;|WR6GrId$f}i>AtUCCG?Yd$olD5SmC#wQub&TWuKQ>GK;wR zZVm5_{=>viL2+AkLWW@X=Y6W&iaj&%8y57NVD%Rk1RnF72(k2V8;b^1Z9~xZxl*ah> z@}o38mHO^}m1};ivg6`<4QS4|ylj-Mc`uIsd=RT!WB;z=8oe{q@tuQx(#pW(O<1p| zoihpBBJU+=ehtf$l%rcozMVB~!=q{BX_Hy-|YFb6Ks^av+(kY3c zW86pqs+b?`^oprh%jgw^J^I$HVUO?lG9p_GH4EBSB>2wnk5d>z;%b&XDj6g;iw@_| zA8dg9-dnixUle~++Vt&8Y{K3)I#luY$cMrHhzSMH>q)uYuHh-WFwUoH%8%O*4r4bp zQbs&&mEGXkhDF|}BF19j4o&1CJD~+NM}LJ45F19qA8wvtxuTUhny z_p6E}b6#hc31H!P<*mG>SHeu~lpMx-o+l=hE*^`+K-OvGi~K_gH9C^4ZQHTu@~r|# zX2weGpz6o&mFZClxJ~_3!@(_G2X5M4dJ)>(bJ;a9>r6oT`MBx9QAugjC=_QxTd$uS zAj|eru~|28WL;DVb>WI@`?{%tHGieI%>*yKn1A+3q#)UJxyeik_O-k6*!+EA{g6<2 nV@|CIm-qh<82TOC`DJ5CpBDbz6Nv=;Ghp^OdpL=92Xp=h(nwZ< literal 0 HcmV?d00001 diff --git a/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/Contents.json b/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/Contents.json index dfe0e10466..2e582bc393 100644 --- a/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/Contents.json +++ b/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "ModernContactListCreateSecretChatIcon@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/ModernContactListCreateSecretChatIcon@2x.png b/Images.xcassets/Contact List/CreateSecretChatActionIcon.imageset/ModernContactListCreateSecretChatIcon@2x.png index 0d340dfafdf3c8aab5af0a718af62a2ac8033fad..ede4016f0321b3acb77abcf5585fb62440660d6e 100644 GIT binary patch literal 1557 zcmV+w2I~2VP)wr$(C?R3pw$@{kZ z=iP4hZg+e3(#h|+o0_+USFdWCd5R=6DB`U&@u}MK9m=>jwdHTL;op^E{`bev+VU65 zxaYOuYn63fNgJ-Pk}NgJ=JEMK85|1}s)%eUY|7!be} zI%RTY`L>}v?)Aj*#`B&fqu29335nL$wIN^PJ^)lxYH**{^}04*SsDJ`KhSpwUxZ{M z?}^aYmd`+b#C?$?OPx||!6K3Fqb#Q$yqk2FB+a~kzFijAhCjspwc#%dB=fWoc#cAq zR3<%GL`IH6kGf@2ZTTK=%a1KlU=m9A8eA2LH;Zi#a-brl0)|ZBNLMznl<~TA{k2?_oN5$y6ojW%QtxA?(#V=b0^;^ zQzDFEC#Y10NU~eXgZIniB!w*JXizC{>qqd)jCh}WtoG22nGf9yuond{x7MZ)7ay9Ms`3Z0JJ;rxj$3^#e*!~ViRt4&$bj9N-0=~M zhs@%`_p?@OH5JKXSZn7q#K9wc$U(^&n>{awno#KoK9te#3XiEF=K% zG8ho6??q3GA`sN*>Oc5n46jgy@%8x*nOR%@N)xXYNqOP(yK$Ee%}|quY*3QNXCl;ph@@lA$)QZ!P~Ob})?@ZbW; zOcBLXw~Xgfk2mjr%qkK8Mn7iWQfS>-Ug^q0G#GXnXo+E=06%82B@Ie zSn%BgRcS9T#dYX;D~&{zanIs9bS3e)lyMLHoU&Ou$) z!U{>W$z6CbRKbSc$8R&}HeMHht_OfPA(T;=q5xPTnFK;AjKJzeLvmk-OdTXM42kLq zAZFLzv!97sOu8HBg{Ztclye_njz8BEK=4Iv_*Yzyj%K7-9l_uY1jC!xUpaA|AR?=btsEIEQ zS|65bAhS@6=>aZDw#N6y6f&{zjttDX}dRfccPTr2lqbU26&czCQ*G(k+LaB3D4;va~eK~C@Wns!+b zTln>ovnrhUa#rnXO?sdw*cbG+y$?VJtHhS?L)c?MT$yxtECVIP+kp~A5LDwIkZMnY z*XUucv2GX$h3)#@eqgr`lw){FA7J!L($Dh2xG=mE66i3){hrkJw@BY#7sX#{C#QU zH~t$hxgeC0!$3o_xjOc+N_@1KKn~@lDw^%>8`E1#nel_;J*9r zIsfUGS9c26{pW}su_JcGj`uSvpccB}c6@?u*bf`BASz48_UoKa^qOwEMul(=*${KE z7!HWaK`LxGkFz+9GdPEfuFW}{(Pgx9~qK+Ht}1i2dOcYK4pF$!%^4W%vD zS3-TX(>ZtRHNWc`xezs1Yt;%x(CQeAA0g^E*5h_GHc+FnuC-p*7WJdnt{$wgeXs$d z67VYO2C!ybt&spx8_+k9L2KX%kD!l*;O=_dBlr`jVY)~wtcAdd4^c6M&yI@PgA<~? zwKBDMf9!)G4&%b;)nOdsAlQfg{w_Ei7a&-TdSUpISP!cp+Up2^9!}>W_yQ%vc(YRi zpIbur@#Q5U34*U8XqWdD1bflM+lxwK3k2&?&QLRDFci1r5p}zcl`&MK9M(gy6(zm7 z=m7|hqLHCS#$Yv^5D!j0H^xw{M%ugJ{-P!~M;crhXQ+W&bWJIY&bh_l)7zzMHow-S zV)z1rg@y`Ggd3vv;{!aVKF~2yZcH>(dm%(K6}#GmHn@xwG%--HD&in&DLP(dUPnDA zDh^c*?Zs0xQ=6+js9uC%o}qu?8<7ve->6;4+}avc^qQLt^)L^j*-CnD)y^Ojy$uyx z08ux4G5=&d}MGF zup6Q}c`?5eMC~%z$A`F#C#-`jkcl}43+;rcE?&&<0#Q2+_Av(;SYaLf8!6}%g@HS1 zwm9qHdDu}s3IkV%9T%;G8L*+0!9v?1s;w9E+d|ZKgME~O4Vl(K7cLom|7js|AnH6$ z7DnfY%E3Zo?~{=YF6%%#Y$#)3@pn)F@lyfbF&H@4I&caO)HbmAN{AsV4fInPcGx|i z(#dFVpuh@_?$h5JOh^(Z^lL$KNP_t^X`fg5a3Xn-G0p*pT&peA0OYqM3?aYhr0^f#5qI zi`|ERAbxV=ejj`I9D;2qUDV{(aA?!Bd@Ohm=KASw6!W3>G`Nw54m#@f8)`Omj-bz$ zy2A#+-T(LEFCaLCb`cP^1CB%RJ&O7A5#$;OlF%j0|FZia*ov~gE>sDdAUJ}NVHkES zj#@KaSO`G|?hm7|4vnQu&$Zt4GxTO)PdtaMa6;547!b_c z>Lm!?yvF%<*nwX#A5UNgdZRh2q7tg09-3;nX?O}t^x8`hWyiDVhf=`|+YINChcVaK z)*XU)aKVmKIEei?fTK7G8(a{JllTIY(I!++$J-#-f-)ZOEX+lUWv9~%Gw>GH;7<*a zjvQpehEw<*EATpIqZ^u_Tqs}jTH+L37;o9meQVj$xp5yVdHfHoD(a#Unxlnks$+Ff zMQ_*8AKLT>5bVbr_!So*>U(rB_JMkc1K$7_1TN%3;K1AH5>~%YFz^La2hy<*8}JGS ip%%)7PqVQjcKizm*?Xvu1aY?j0000f3_}SD2qP>8sY*l`l`;|@p?yY*(u+z_ z5F16R0U1P)HVO(shptp16s3sf!JGRY<6X(k$$97Ov-dt(iM6phK_bc%aX1`_X>Mu@ zK7-KrW&*gSc`rwBIJ~vZ&!^15S5*k1a1jc$MuCJB4onV1Z%P=9#jxE2ga-0x65KGE zBP4@IAOKyQHIO8O7VHrfTBCAcDi@(fAXEsUUPB;I1Hk|p)KDSNA_TLjpbNwRAq=L5 z!EzA52T&*`Xa`AyOhhvYA%hw;DU^pmj9{#Ua)4(PA|HuhG=yNv21!tZB7iC3XbVa{ zT!@Yam=JiO#1sGsR-+S(0)q2zWf+XM116CORs%$#iWC8{Xk-HrFb4!s!au}rs77yW z98iIZ;=m>t6pafA!*n1Pf>ojr#RQ-lm8KAlasYZ26Gl-9m}(l}yx|G@=m%f~rU+dM zObZtR)5xG^!!!s8hr!C^fg53%3E{AO1d5C<1!TZ2vM zOArFIgQ1vv*p^Yp(9x)A5UdR$$}yP95TgEn5TF}Ji?Kao{LwBn0Qm^Sq3GNd@RMr3 zWNUR2xA~CwACF!QeOg*0n8de!LvGDT`Sh&!=7cyJs!=5Ixd9)O%w_ytMn5O-RpkwO zjBgUZ311`9I^G&kmHPfx9}&t9tXelOo8{-7zTluZS4=uA(pKYv*|O#O zjeBQb4?p>9B5~HJa6xpEUM^-OC-GqZKlM8$-d8-cxSaCkr>Mmpf=d}k{n-7LrfvuK z@DH5SvD@AG-*)MTp{`ET3dfJU$-cwTw_j-rPt|qybeG-wi{=)uu2Q|R=-dC;A)H^ysM|5y6`Xe;i8&*L9QKe+6k)I#Kq_4=$?{8O8(A;D|O~w6b$87-7C9B*UbXTr#N*{`c6_7W9KAv(^%4Vst&tM5_|(Rf`S85 zC%3kxGL2+gokhkv#?-c_)T#%WYd71d2Wr{qCff#?&MAl6W~My9qu-ukbJ~TKY3s;x z+4sST4sTCsyDJ};eb$n#WXR|Dn%jmF`1*1Z%lh8FjV1S4Z!3m0Jd*IQh>z=A2{d`O zTutN8K7ewO~JE|L})UE8o}W))dbCo?B<#0~<P zc8e2MMr+zwMM;7$lg}+;@Cj%9lSn7@avm!{URe=|{B?I>S>NP>^hoQQ-03lEBhL~m zPK!0J`RMZdoU6Ju%Hs6MT0BE0r?;YaL|XGbb4u>snr>g?3Rru|wptC~{`9>`?!0XLH;_LTz{rNr1qCWWbCF!Q-YK^?9+QQ&7 zPd*7RdW8~o+*|FA?|2uVI5FM1NO%X=FdV}2=SDS#R&zM7zfV)O)&@1TejH~}V=)r( T%cA-W`p3dFvob9{=F0sC@Ji}a literal 0 HcmV?d00001 diff --git a/Images.xcassets/Item List/Contents.json b/Images.xcassets/Item List/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Item List/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/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@2x.png b/Images.xcassets/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..76fe6b80459a4fe2cb1634554c3cb9de9c44e50d GIT binary patch literal 448 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TG)d4;su0Yyk^>gEuK=dqh)qfzv z1c+9@FkS)Tg4rM`AOf*h0MRqkRnPzb2dQiTk}IA8+2*UBS*&`t^46C_PrrZu`S<9v z@Ahk-hi`wCckpfY{x_TNeVu;k(~m#@mfZM!`OVMwKmK}ceDU+oKSkT@V?gUgOM?7@ z89W^?uzi@~|C;%j!|XF&r{~x+9%sspAz>y=H=VuKf z&dRWm(~<96wRP35JauE6SKX_utX;XJ?B^e3N<{ z{)mx5^vfYlrU$E8miKSIddm677lHZbRvj|>V|#4%>Qeao5Fl>;}D$_Dl9%?k%10j{9^;wdhvM>ynjI<}*(1%TRouY3Lqe daWM3Od{L~)##)iGbf8xmJYD@<);T3K0RU{b-zNY7 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@3x.png b/Images.xcassets/Item List/PeerVerifiedIcon.imageset/ChannelVerifiedIconMedium@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..277f37da96342afbfb0b58aebeb21f157d4cdf87 GIT binary patch literal 596 zcmeAS@N?(olHy`uVBq!ia0vp^x**KK3=%mav;s&?4)6(a1=1!fUzn_XX1(#X$;#(u zt6l=xAhha*@ych$E1v_&hX4OfRy_az|9|MJ|E8;+S+4pIR0%}JE1sLI1}T7W9rgp2 zy#NXVb%AA{0kt@g)D<(=wG*O93}O$Lv<&R)=+0%$~i<+i*f^msIgKuHO%u!Wk{%kFDg^G#A~< zZs~uhkZGri*cQgnW1`=e1E$_{JpI53ErdMbt-#_}lxOUd*T$|~0g{M~R zns^XQ}-(MO3a~Uu4XkVUrTv@4TOT~FvC9N#odH3p$NA2zLTz>rhhN~Cv ze2_WjP#nha?yRhj*pn!Tz>yoC}#Xz|ABGNP9|pls$S#pq$xJjtJfVU z(>wd;bbjTt_wOg~-u>#I+?Dq?cK`nUFLy_M`IFgZT;{&}rst+cq)3LY-Z}kyzTwpV f%X90eY}Le7!M4du6{1-oD!M<55h0T literal 0 HcmV?d00001 diff --git a/Images.xcassets/Item List/PeerVerifiedIcon.imageset/Contents.json b/Images.xcassets/Item List/PeerVerifiedIcon.imageset/Contents.json new file mode 100644 index 0000000000..036124615f --- /dev/null +++ b/Images.xcassets/Item List/PeerVerifiedIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ChannelVerifiedIconMedium@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ChannelVerifiedIconMedium@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/Contents.json b/Images.xcassets/Media Grid/Empty List Placeholders/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Media Grid/Empty List Placeholders/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/Media Grid/Empty List Placeholders/Files.imageset/Contents.json b/Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/Contents.json new file mode 100644 index 0000000000..3294d75dff --- /dev/null +++ b/Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SharedMediaEmptyFilesIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/SharedMediaEmptyFilesIcon@2x.png b/Images.xcassets/Media Grid/Empty List Placeholders/Files.imageset/SharedMediaEmptyFilesIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5c704ecaa2aafcc92e18c33c3cd18c0a71744fac GIT binary patch literal 8892 zcmXY1cT^MI(}qA2Nu6M;yPA|Sn&P?S)m2SJKTQ91&GNEMV02?zn{ zT>&AX2-15mU*7Zm_MF}QWA5&qGk5Pi&pflQ5c*nBDmE$-5)!D6_5(xWeel1N0z|yN z;8OfSLc$xR^FY-YMY@>-L@_$oDzE}KJMSYc=IRF*-3UKgKs;~ z`KnKV-MhYkE#DGRSsE*Q$266iDmh_=kSH7F$(R(&Et_(`hir**4-gm?qSD6A&_lcY z41IwRfUQU2_B!l?@r|=Pv*dTrr&0EzA5qYNn=mErvF7bI_8NO!(h0+D{n!0Je+Cui z=iAH`>t>{%*w>XBs|u=zA5N%Ehd3k zv{E^6&w&?J5)m-#qs0=EuF1_n+S|1^oFp3yY^92oQ(~9(+8{5OG^R-VZrvVsNySzq z9%GAN?#l>paB$!kF{{pJ59#JU&{jJ6Fv2eWU|RM_D0g$F{un(=uTv1NBs zsrBs#1_n|GY|mY~pim79i6Qj7M-Tk%ZQRP9agL0P2!t<(^*nAv&-Y}ElxFMG1u>}; z2rJLRP92fj+S;W*83F|-a-g6A7)G`7g@N$WKJU=HAWCgmoVRq8!};k zdWinLAE`V%FeslC6`Oe8;%NF!2!?DaT&RDwz>#@yqLlRv4WO> z$#U~ng7eLC;)u|-p}q#`&G9L6+uvDIyY;k47ETKlZC%}SI4>_RtW!4o!MDU?TbF8! zql}@K{|F&KNc z&p^b$L;N($WR~nj>sWEG+{)<3>XYxB0197jw*In z^y8u#P~f`U+Ib*x+n%53Q17gPukW7239IbmyWaBAUa-t>Eyul@$-;vdZ`jI4FmF0L zBX6zTlYgARgr7rBc3mY{H@wQ zQXsFr*3b+GVQotrbjt&kyB5t`GO5ycP$l;AYgciz zA#=mR*;&gkxdyOj+2E!!cXHitX~#01iAHuhBPy8A;aSDM1H;5+`t51#LI*=fXt9W2 zSTa(kl)tF#@9$sx^~=nMvn(^zruT(!NJxmg<@#cU@oY95Ta6b;K$^CYR9+fRk!%#P zf3{x5U_${HYyi)Wpt-^t6lLP&?I)1bJWbUW$3*K9+ft*x!6_Ml&u`rz1u3vTp55Rohn< z*p9^4&}(+an0?;tD(K2K5uG(inpiwd`!>tey`~l zxKqBC^AU>Mk<;a8>YaJ)dINt}-3m)X-*1jB>K!U|d_JC=@oy6~3Hf1IM~`iOF^Sy%nC;@kivaP$U&K9UA zsdtP|X69cv+~3SBf9Xb z3d!oN3=td2%2y{huDA*^VV*VybKTIkRO=nmHP1GuKU`h$EHE%YOMNBW?PD5@dL`g* z|A&3S!R`0RuoanRijl46Z6zZ|$7S&bM6;L}2)Bbbn`1}YyqPHckVph1M`a%NSeMh& zQ*ZM=7giVutS&yyp?6D|8>WCleh-cIFw>Z+vDXSlqD>%4CJxB~jRo6NfyZn?Fe;w- z$Xiy)h1SP>>C*V!;-Qtm&2Lkjn3RDd6B+ptb8G9i&NaG9RTb-PQ(KddKl{Ma z1=2@#_b#FyKarhYnOrduRkSn??<=0*qeSm96n^(4@3O8NDbxaE#$syAX=vCy1D6ji z+Ox|)emP)UMK#f;kr-lJk1j~!{5@=4Z+18{DqVQFTa+SMDC6cccIRjP!(P-NM$Ns7z4JJ8ENJ}0(*#&qBczg{rbcK zZXLKd-G8c5Y2rnVjJ}OX5{86M=Bi0E@>;&LCJW{25dMdPv@#kLhZ$^qiB;dwP4*-~ zn$g`c;pgW+z@W`fE#}v4PEQxQ5+vOk1^=`rp8cYL-aJ%t6DzgVB|PxfY)Lft=KqbO zz*pxN+VK4Hl@c6oT6PsuzCKw@evAmKtd@tr?>CA{-$H+^vKK67MUW%`?j)JGj{w4% zZgBqO##fiwu*>_qaII&QjLcx5)hoRs+S@X03Y8}VmA^4Uth6?dhcs<{t9_-25WneM z{f`Ba^1Ov6h<#Niv8z`4&`B~Z@ibE1moaMvF7{~<2IiK0;S-)G7gS8`vHIM< zyqeIWXKg*lsPI7#Q(z)4WzGI%qB>PLB65rDT80~5-Wq%y7I{;~YUR1**}=a>T8+5< zgXlx?9Z0~H7g6#pSf9QpO@*V&Ymlot*C)8M6#eZ11BxPJ_98D_rvg%4gl z{pJ@QnXnUo`9qxFJ`qIYK2c`!66Xj`JTTk%3)ti37=9r0&!jN+p=z~LRi<2(W&yTF z6Nm;1JM`MD`{W}g>}30NhuLL3T)^O~fYu20rotF{`+%=vFLpCg$wE2b^#i0hsIPAm zK15(`FIP@i@(eJ8uF8G7V`hc6N5!ZodSm0T7~sDFz)_E0i)scH%Kmv$CN)RG}8z)Z<`CUl%qTA}Ka_ESgI`R4P z&$NjICgG2UkkRtVil@%94+d z?~?mbGd=__2ETLXPJ9hj6hpVb!b@q!g=;58Mz#tT)2!kALo&1)*_6&u0c&=Ad_1qW z_cl$@JC)$~FNk~_yknzJP7}q~5@A=$+fratT5wlbxY+*YFaK5nsw7C}K$h$n0&yQ+ z>fLtg1Q4V99IB6wb{z!LL`7}h?!f$*|MpkWr3k>wHure3P9E;-;(%Y}s%#7o!2<^6 zIp7c)Hl84M9YHh?z3@u>l!hwy4Bu1mCMv26uq&6k^Vsr!Y(U4mzW}nLrcbt`ewSy5 z9J|r~y`4`F|Jj-JgS37ZNli!W^%OJgj?@h#Ik(+Qi|YKw8EVlMF$yz^43jBAIH$R} zxls-=F=}l|57#0vxNt*kgm9DJ7# z9>bViu0<0g=VJ4|U0ZjM5mwE*ENx+K)SiU<27`(DN~d<Mcwi zDm)qCe7iDB%w%e3STNV?(q^H=Q$2nCLE{x58jFQ}oFr%SqXH!TT^yC;){_83W6?~e z6LtOewz+=6R)P9tDW*O?yB0VA8r$fFWwH1gGe5p?J>F1m_>J%FUjaOU!uTx0Z)>)( zf`7=>My8$gmDg$r2pv4J2B1bXKYq`dpE=~}BGT@IzN%9lgwjNj zb&=?_v!>_V;dglt*SCOSLVn!U%Ux6GSAgGW(78{atE<4aE9VP_*~A=_I66BU*drTy zjXX;k*@W2Q+8rA~!diSOKakJK(dj=4A>Sg&kfmz!d%t%=m(PfWmSV|Ko3s#w@#alo zM`Se0oHVEBLE+n2^);W>(C8mm<0pqeG=TRwZmlt2UYY$-v{(8~3QLu?Y9^jEtSwS( zO1xQ6uPGO#NXvodj7PDBVhe7$QAGeCf4wPm|KvfRP&xqEH&yipq(fxLKm6$Y$U|@fL{#^;{b<`KtO%Ke5h6EBrD7Kl8f*J1_nRClc$SVXy{LxK z3V{&I!!|yK&`@uQHvF>GnFcPAIo96_%69nZCClZE4d{(^2mkEp`4xT4?E={YHI9cH z85({y&nT|f60IFsFIvXM|3+@Ah+WqmcQ?W|;$!uP85Q*npKrb-d}tUn9O}F=&k=&& zvHChP9mSm}NAudh$ZE8bo8b{jPMZ&1LRX};+i#5a?WWgFJUz5d+WY-94mBr_(J`1F zt2O79@o@XLKe1hQs6Lc_-=Eo8CVgJ2lhSCzKDQ|?F zUBXwJ3E31;3% z)Z(e!G;OJT=d-td?%TQZ{c(ruqW8bLXOSF|m4!Jk+Sa(a?-dEl{#v3;^w9YRb*LeJ z2cCOB9Dzi-=H#W>V^=deHYPjYP)@@9b$MmwWr6O%d63Cr=Dz!vXZKUhE)9Rq=V(Sv z(S8jz6VUHvm0t6Z*zax>-C!+@9cJa0ZE0vW$4gPH2lW#IJ@z`_BG=42|`H zq0ER#(vUotf*{3lFy?E`K%+W$;5K;{ru(!R9hyV;OeV+&Gqqr zmVuo6$Bq{9pYW)*{lWV_XHlJa7G}=La8j}x4zxoRpUYKm7O1==ez&o>ly#&??_IYw zK!h?Qh0H2(K6@dQ*$D|Dry-l$@>_VK$FEz^E#+eQgMJFcqFPP3eGwY92;bQeE(i$?4IX`xHI4BhlWqz*-FtCK z+>9)o>J?{?x|1TbvE)68k?Q*}BPXZ7QvYx2T$bhN_5Eid%bYS*KOZ(T;Y;+ju%p(Y z_l8w!R+et^?I({d{Ph;h?1X*_%y)JtvVS>0T$g>lB#Qz4cF859ik#8Nsf7_ej@!Wu zSusGC+oeRQAH{kONzl2gO5Tex5bCam-wJ#F7p;d zRY$OH-m?4|((LH1kU=(l_Y?GeqqevMlt7Fb45>#-7uV#UrPrTvN zdhq^5{iH6VSTw`S1pCi1+Y>JgYBT9lI>2KBYm4J8N5sn46xZDy zzdzYhIaz7dT-JWJ_T^gkh$t0Z)n*&wprKSq_QHB37+t{J#A6V9&k&Cj)|J^?)Rj=pbQIiWp6j z!nxb`?yaYo)oBVSvYP=0J%85vy?yr%_mqkz0jp|1;qP&BEF7U;Kz~DQ)G}^s|MGbL zcKn>w9s~xp%`6IlD)}1;-dHmGZ*;_PZxdd7P?eftdkn0;v1Ylbv{`$E!STW4e z(NSZ4vI1;(+uxo#9`KfzbdGdo<9F>ok&xhe@QCF78Si#srX0YMz6|t$WJRX`hgOT#4|4q$;Pr0m?%1uvy z0u1;Ed(e`&e9J|c;bY2ri0wQPydg;!XHV-~YK(Vf_9zWFT4^jFxbMiD5>X8&h{yaW zZ8EL6k!qvp&DZ-}LPA2Rj27tSL_1 zs0j589!%T(-88B7C7HKWPphn3c?pzWfrr2(F}uS+Qhv9Kwc{9qJHXj=i}REUTjuNI zQ33#;x2`s^e2|)7j^$*(st8o1k#QcqHDj2To^DM6g>F_!`MFgT{gbaEYJ0gQ>ypNO zB7Kk9>kKWJtW_^_PwGD*^e|dH^yeMKA`p%y%2k)dqRZPpLnYN=uDi?{$Ike~*to8~ z4MU$#!Po6a&1c#I0zPDbzqC})S4`!t$vd;;g-Ef$g*L*sNZo)`WPwlTXAo6U)PJ1% z-;aQ)fK#5dNr9pf-3HJWFJ#ZUl=nxKg>w)o`j&@3kw6>7m(ZrnD4EaC&{Zu)>gq-n zkRFVkm6t^Xh@Cq|6wDi&ByxhUlJTsqW!=+6bv?l5ie-6o~mXI-tT3=+W zPgPyd9N`h(wsLR^TIg}<4^$K**sMdqmS<+nrZ7k4O+HCq-_RK|Rf`JGj#Pc)At^N4 zs43cQJb|E)@>LnS2qB?yCjy1?;@j03RUU3pMQT8h&+Q=0@0VZdo}WY1)UEma zL~bB|%%5cTWVpqQv*4>MJ7nyB#NosqBvfb6>9KuL=x9yW%oF-P5LV0-wJ|WWJtP@F|vt#(y6Vf2>>Z_NYu}+B4N!ntX(0 zA6~wQOZFyZ-=JwC8~+hxB6T_AbD_SVW}o}#aKR@Bs$--J1x6pee~fepd|k9^gT_~j zST-QP>bw)U+`SqSR=GN>pD+usld7IEQVeQA;?wNh>!wSabLr#ckJYtTM_u*)jL>bp zx!i=pm}Wc58Mp3;8Jh&rj4O?}I-d@9N`jP64fp1K!ic%i%)8BrC@vcz8^e!Ux|8;E zs&UMTAqhOy0`PJuyG(!gX(k-;o^&r$o?Z1)MGsfbaSe^imxT+pbd5;}f$F-`*_3t2 zN=RFh%FOUv_BB3D45X~7gezxUA;-JZl{coLp<7L zAy&c%q4Xkm=qLoo39LzloH|PS32hg9eRZp=tK@)0+ESi7_wUmx(Es6`(VIQG*oMT1 zhV#9)V3Acpd~v4BC2yrVlo|CwdB93qgNVyndoEV;MrAQ%haB8R-i1{dVu~%k!qNv) z1VNXjKTlz&q65nOvIXhza6Ju>nMq4?;QrrZn?oTg#*LO?1ghEwzxJ0++HIm?n}y|^ z1l$t!1i1>=d!+;`984m~p7lt+wZfeu8`J%pu-jn~a#9_W+)kNTaBYX&2;c;(OOqy$ zQ}kh?V+USVST(-@eIKa<#uR#@Slj7rasoLGvM)VzcSbrFZFJNqqwF*4pBl!K@K%jL zVU}zN8Na{%y}zn|v(2~whGi^S*#d);luQ3v8NAq{wgS0Al0wy@<}|sGNP4!)l16q$ zG&JjKdd7u?GhhP=rH-fclJr_Ma%agtV`h=h=@|#YEVr_wHNy29as?osn4E&f#!5HY z6w!1}K6HrU#q)+>ePEP*FeRPMsG$VdUJj0w%zW5VqZxp>F9e9j56umH6>*N$`rH7F zwD9K~d(Q1|%4LuAct)-w$|uaT81~UbE9g-)Vpd3u!<#V9>RmnpU@SErJcx)*0A1w^ z%wd&$_|lWFrq4`dL0aLF44J+xUSDCC`$Njt>znoSSHB%3y1uB@3UI$%XC#GhKUS-c zyD|bpVmh!oAe?+h@1xC7N4DE41ylh}J9sQZjoT!Mu^`%Ma=;nxzu&RvAi2%G{lq|s zQ<~j3&mEG~CI|iA#Vs}6{5cJoD8f@(R@Ox98XN(Yl$7E#Tq)gRm32jDqMcw(f#HuH z67u^Ryl9nL4;vXlTFTz)D=u{pF`rO(qqPn)IX;NlcDy?Y$&@Jrgi|iro)5qN;uU}F zN)^u`wg0AFvHrN28ZuZxx#B7n*T0++m>WaQY@IFTI+}HIf}wfeCuPzCF&$W&KR>bC ze}+POAaS@bHgW1Eno2RatSH3*9aSF&i>LxPUAI$GQZ|CQjAy7Ala%?M9Z4LeoeQSb ziy@4Sw^uod!8_&L%liUJ8px$Xq-AHf)De3^l<@O;5Mzuo!^=U(jrzU6v3?|^W^=FG zE_2gcwJO+}dmaMGL12dKC@^g$o0tuTJNun0b$oZ8D#_KO*#7C84^6vdV2J%DM*ds31`-nz*tyjt0FUT;?O9mJR+Kq^kZClZ z2i1kHja{)+>OyzBn6I0N-T9&X&?S|d9-3f{ z5Kxb~VZO8zO>|i&LGp$A&?u9#AAcAadv`mMz><*`D7hV@AnxTI9Xm$Om{jm^JwHFc zS&1mg)87x+y11Ba^n1!v*XXdFpPH6-Q+50Ipzzc0{U2mJr%yB!*$z8y_-Z}cv)`4J z!4Az*w37{G9f-8}h(yyB-0nc!f-GKW4{y5obJ&R3V`S$;_GgJThm*6DlUI}fKSP23 zBGd8CjZol0Le3)t1B27rDcpFMzzywQPs|b0Smf2a&PXD_A)Oe7yq`A>i^Wh2nQq{y%1Xb-mUOa zF;*vMm|-??4YH;-Zhm-W1^>jySt!gYNi#&BxZ)8pMHf587(TVCtKws_EHsIZkp;bq zeh#gXT7MP$k4)loI29l!pGQ`+&?~mUkoo{bU#k6)!Wh0a1H;L5*~buSq8L?nW|dIF zxhPMl&PiAPj+1f$$HUK)jRhk?bfwWDBbZCX)$<-&$Br5$avEBNjI6S=2~2eXvp0k> rhH5$94fW1fqY(l3AfxcUCi^HUEcYw)9fJ5*3W<(}{)0+2o5=qIrHJn$ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/Contents.json b/Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/Contents.json new file mode 100644 index 0000000000..3cac03aa7f --- /dev/null +++ b/Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SharedMediaEmptyIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/SharedMediaEmptyIcon@2x.png b/Images.xcassets/Media Grid/Empty List Placeholders/ImagesAndVideo.imageset/SharedMediaEmptyIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7bce6a20233f54ad75af0baf7542d981bc8b00 GIT binary patch literal 2296 zcmV@e>Hj1q|nk#sQ_kdscflqjo`#75eC{5(LsKdp4KtGn?$+3}HH0KEpr$R2`RpK1J zXAYj_7|gTmnM;5L+{hrjz+)9Za%65GUcnf=5aSQ_$Zo&`XoVMgEaQ0!vK8@8R^T}r z{n$7A0ITy;ifON74#Vh42RhT2aV$=8{SvOrF2n}(-r{68(u=n_kG&}+fAbv`sn6lu z$`4E|2lScjLEMAUdXkLi*;Hvq0jKfZ78CtL^0NVPFQ&=q4dX`p<%8XMpOw)MdC~eLV+d9`R1cUUp-V;h_`xxt*v&Tuzzc z<=nV-HySY#@r~i9b3BNX1>#*P?%IZ1d62`>@V5;MeV)oVCOZ+&WdR-@n|PbNz&;-{ zynT}i#Jf}V>3mJBD+7D2Vf2aSP(3=t-7OG>xY2ig%QQT{Wca)^vxq|6>3;~MwVH46 z+4wdp#7P429=ooO?=kRXI{uAK-lzzAwCpB-yWeYl|BIXN=|M?aGkDr)qwJCCC<<`_ zztfgSC{A+@Cm5T8i(E%hBgQIf8&Au}T^ipNov5*kp}rY>9e*vxAR7DT(8e9&1c7*8 zin}UPo0?Q|4*9un7TujCsy!n4?8^f*rXQ18$O0zNi{H3~U7bO`^*C`r-a_*_TZc+K}nx`;c=CFDVIWaYko8PZ9L`s`@?c>CmU8Sh#K*9i8zSM{T*)VaTk+P_h#AlFTea} z&_K@ad7p)~X+3}0iLthb;yq#+PwPDn)%han>KSIcpF`-*2G-Ms{q4XQlM;o^j9YV67&F_x)!0Z5s$t9!hycrJ6uol>$*% z@!h|{?o0tUCV4(JK%!bBp2BK@XrHLM`|5F`#Xb8c{qwdW&p{NOB&ra5u-<3ooRoKL zm$Z{!!``;;wwH*H${ph}fp}IT4RJ`Y7jV>MK$7p*ZJkx5mt_=BPP-O`xX9Qg?Uv$A zR*xj#11PX{cB$p=<|Of4Tq_VQ#iz!3ty%#Qm$7GnrCcD|M)CZ#5vwuT==$WBgVM^tYcM6>39I-yr4SPpzDT)&D^>p444f}V; zMV$-gM1i;}6k;XX8}^1%MgRJgi0`E5wkpSQ0<|m=SBFB(`4EXIU(W zKg&a(-QqwzA*LvHpio4yOFW26oY$g8EU`p<-+7`6#MR*rT=JQict?rYSzP&nYsBMX zLhK|EOX(5^;_;#u*Kke*h{f?E#+|680Hc{d7*YUU#Tcc#7^SM596rCH44#Q zd}@hU5)b0B5g>jdCf->rH^n+p<3L;+31Y^gcpQsG6wi+UvASg~e&jxi@gQ~=h$VE4 z1MwJvxHcr>M>2}FL@g@BkKH3KagO-0UT$aARfWuSP;92S}dYl9Ef`|R~$--08vD-SVr+v*NF8QFQRx+ zsBQR160v%w5c6mm3Q>rs#l*W(Lm;+uk0``LB2mL3?!a<^SY6bjLj24<;?7BL7}sk= zdvS?4sw)s>qJHI;Pah5`0^C%Dx;cxE0pFwS=`l4uy_(dWL@l#UQ zGvA5bQ<<^(c&;@fc~Ky4&nlx?yr;v*(%n47Y2q)1w2KN+h|(@m zr^peDXd@86jLu;_qiNhE2E^71k-X?!o^Ck1GU`N)8c~Qxjbz;+Qp5t z@?wwTS8ja@(I%3L)QGJF;@7Sbh3Fx+#nB{cOBu!dMD$`%9M4D!XcZTtka1G<-KY_x zPgEhJuRQ6UN$6KE3~Vmyf|JE9Y*9PM%wJ)#iz82x~~9NMlX5xRLxL@DC~qMJB0I>d~9q@63xk8V3aY>~+*3Q>!BMyKxN z+)Y&b|2;Z}>LC!D3B(@+V!UEURvDA})hXil@zHmdkIF}`IPt@TdMp)pj zrwf-;HIb{LGAHmp16hHtz%D$^zKQ=0u_LE(CvO2i^CO?}3fFTy^%B{k|LgB5w#H}@ S=tnL900009aUVI)zY;I~1l0Tm^7MFXAlC8wvJS>**pr z1{)W;2R|(<>*i`?SvTyW`(P8LPJI;2ap#rHQCm?DPNCG&GS}V6Y8d?ioO+=vzCJEu zA}^I|DX9O%B$qI0H5$R`7ka3F(FIKG0(ZE)6jV1csSUrQ18OevFhY%{LVW>FqLSL< zT~$nSBl7|F=mRx_U<8%ox$(2=>*FqFE0vtCYV7L|)?#=}~pV#v&p3K==TA>4CA3nR8W6><2jVeD1 zxrncD1HVs%1Duxz^tS?9VmI_#U{Q9yLl*F#*(mKC}1TqaMW}Hrpl+V=b6DA>_nr-nVdz- zPz%lgCVKiO(&W>FIP1eQ(NCXEKgLb4qfGxqe2vQRpQuifR@CFq_|84*IJ4>@8VqRm zGBFn4qJM*pOF4I(S)Q24)S|XpJ|adxKr`4$CL;PLEgr^|vtu1$R{O_TJt$udc9{8` zN`pNto??NB!^~#-!I`0!Pne+=CzS4#3_Xyvia@0qSpFE`-REDql zCp!Is#!dW0pN)PWwoc1grw7qw|3vi6j4u~z)pE}I@KtviXPJ%I`Kr6$+e`}k3N+30 zOjHMkcM9hD0iBJxzH=}Oj3&%H^Az({jHf?^JjYyQJie(?s0F#=%LTr|25Cv5PD|f) z1G76fuwgEnWP+(Yj`*qP-W!tt3hJd0D0WHL>5V6hJRLn5XOPv$gI^$;9g{1a`5Vq0RS z3}|!~CmYif|xR=DEIN4^K1Uk4hw2Hx$q0_Eeafrq32_+8Z`A zeSPp>!E80NAOHu=GmSn;rZStMYCSlek0*FI-!jkSte6q8uh|U6CAnCK*@ZW_fE!IP zUxk)|9ewBiDmP{=uRWUSa6!VW+QjSMjaJvn?r6Jj+|S7sX2jAf$#39boY&Hy-%n7) zPOvE7vKdMx0{UAm<#3*;zc=ue=*esE9i9ftvG{9-GsSZ%uBy2k7@tBUW0^9XMOVPGP&@rN@f-MDUn)UsrZYwFF8wp!>{yI2 zUU~(4@Bp0gOc_j~hsK9Z?Wj~I^aWI?Mq>o>)Lc41zoj;?OPDhH7x5Bowz!QcsPwQy z$VTxAc}CyB|4f%jVdQ1DPQI3|q{aM$8m74`oTZkdHYH{~wz>B|b9{t4Gf29RU9SIM zE3}LPh{Pp6#wE2r*jw{PI7=_W5h7egC53SZH54KrmC$E@mFphG{{Ubhok$M5--iGI N002ovPDHLkV1jGwbs7Kw literal 0 HcmV?d00001 diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/Links.imageset/SharedMediaEmptyLinks@3x.png b/Images.xcassets/Media Grid/Empty List Placeholders/Links.imageset/SharedMediaEmptyLinks@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..774bc10f816d753264877c5237409ff92fb6df82 GIT binary patch literal 2774 zcmV;{3Muu8P)c&*R>n*F+woV3?_s%h|{ilHo>P&P(NeSu6|XLGwL6 zA=#e_$1`(FbD>Pr2CL3AJUalD?gXjjQk@jBnS>BPfw)!@kIWRQCwuP zIUN<0jRzGnmYW%tY2*{U%PyFGCBuj&@;ly*X$f-=%)HuvMLHxE45I?z-FZRIKXrmv3vT8EsR-n}o-1jxns+RBwXY|KP?03| zn{?o>< zdbO6wc?>Obh*xWgKJJyktFs07FhN~H<(tXe>m_b*~{oK6qn;Ubf7D*<0jNZ+OqAm z^}nW$+(7QuS1FpEyNDf#`_K-Z5OXk+B0?_ES{4j*WWS@ncg%dRi`pNd4Y^r|2K1%~ z;JHf6mXI6BF$^V3=lAU-b`#7-{ydBRWXbda(x&{6n(m6+X_{QQw3FH$kU?R5ifXb# z?$cUkE+iK~FaC@64V13zq_znIG)L+|K^hw^iB z12b89fG{Gfpx_U%#uU&5e$Enu5?B2Yl__K3RJ5z zhYBRU3BS%zGm%_@TNvFXv1deTfe=fT7H>(J9f%Pq-u4pu~jQ8Qp3qD%pS$4U4s@BNWGm}UgO{<6B$*Q+7NGIHW-W~ z7UFB<%@tN^*PA+WkMt%I=C8IDzXEqk5+5Q@F1An$GmhLumgTQ@PWJlleBa( z$W{2sg4AYZA@ZOD+m@kPB5ANu%Sa**o-IqYM5ZoJwQ5?Blb@8OTB7%=TS6PPj5WwX zJBF5_TD_>4zuMWz$$dqs^=cV!A_r|4Sd?0?_E!jzgC7;8)~lU^R^(u|S6eEz8EoR! zmPTz8c5qv*SL@YsZ)UHyG-|h(r@h*es5LwDCths{)Xwi~z9(&wtMzKrm2xRBWkjsj zt4$4(vE0aTP_44Wy_)w4jxfgu2lMW!k?Bn~hFacA_A=MYVhI_Jt3|J%Mketr$z(65 zlaQCn6VhPn-mBgaYTH)LTEeRsN?cN#uH>V6D*uI%D>Kj%mR{{wn1KSthOtyTtxE3J zmj>(W5aOhk*Yde>+@qW1wG7AAw(`;O+q6H0*D;(|+a$Zj?x+d*lHtBunVHyv;zW9L znaOZpt$Zox#qY$VCim6yI++r?t$BPjx4N!Y+BumR9mCV;@M`60-rlP%iCSjv?bVh>?Y%^ftMzKV+D(x0YD;u417=62 z*1xq`XDaqch+h?{)~g+u>$u7{yxL+fYQcTT!R#W{dbLB*jviTKU9Wwwx-2?`Y;ZYp z$+4bgFm+{ml^dQ#PLft?8PZUmYPUrPadhjNHVGv$@I* z7b7n|?4tKq1*m06nBK%iwROBcw^|)si9Gol3;!c{Hr_;7;AFXsBG-^Zc#Vx(BGjWG zTCfHhx>|n0$UkUBi5}^uF(ll!cqq{hCm?|nHQ?ObEydIre?4a$yk!ttHwz^ss+!UeO zt+X~xA3$!9>c`CtHFK#TP=hcUwM4R)(xw)21)j)I^LpXmLYNbcS|W43(yG_U4bqkT zk*c;iIfM!ZBe5bJZXE<5N4e=}L9e{$J3GT)( zrji@*3~i0A{YVo13+fzZW0TGYr0A!3$T6eP&$QMXrsZ*P_r&E z+OdlQ;&jy?-s_C-FgR)t?xA9ZzNPp{Srb2>xK^3))&)&f-Y zc$LbF^ZRF(&5$q3;9`25h17Zt2vl~a-Xy_yxRYe+L}MiLq~_qmr44n zbKYJq;JMPqC|9)#!3b_<`bRTnyJS^|$6uU&Gn+uED9^y&%=M&8LFneG44FCWD07*qoM6N<$g8dzAxBvhE literal 0 HcmV?d00001 diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/Contents.json b/Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/Contents.json new file mode 100644 index 0000000000..6ea4d48f35 --- /dev/null +++ b/Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SharedMediaEmptyMusicIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SharedMediaEmptyMusicIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/SharedMediaEmptyMusicIcon@2x.png b/Images.xcassets/Media Grid/Empty List Placeholders/Music.imageset/SharedMediaEmptyMusicIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..600075ae533b2f2c65b1fdb710ef64ac042e0a3a GIT binary patch literal 1112 zcmV-e1gHCnP)-lf_Gktx-#=M!`rg&_hp9QWL8rjU+`x>GpY0)7^Hr-I;m2GfjRo=biav z-tW!Kn|;3*1Pi2OYOqo5#s@sZtH`pa?>4WwSRB^z6?CzOmpO$fxAHabxX~iDHr%ET z@>2}q?|g1CQ$xhEb1%E_9&_eMaTv!lK{cAU;AnpGmGlr}uPDVwk7cp zi>h*Z^p-x#J|1Nw*B5(e_KMDNe2e4U7e0zRO6bu!{RRh_LOZr3S84X>g3d_{Bf}1E z&yQYz>7(rBQPog9dcC4^oZsL$_LdgC?&*KvJX3rVTd=aMCkpY?&7;>jy<+D$LKnKw zg)VfV3ti|!7rM}eE_9&_UFbp=y3mC#bfF7f=t38|(1k8^p$lE;LN5<|BYs(sbk&TT zWq2{R-0WmMzc14}z2!rnydAIN^D@4U95*;!o2*w2>JWR2CRO-DPcKtXs{}sbIbO+& z>BwVxW{GOX7bS7C9?>&P*uin9uihK}{t1ja&l)iKHQVw6VByf&=1H!G_R9EpPS!=` z>B$F7=}{FgILwgYR$Mcst0rg3J*hkKe*P_{8E9vn&@(k?pFI=nE%H_~iwPaeRTF#o zXQ764=;5|F(o7xI4gnl_~sFpV+LyYd?3b@Qe)H(j>yY`A86kx1Az zrn%9zOeg2D=@H*$I}VyF@po3eUBc4;FW^d>P%+vFG+Xp_i<_ro5B9f>9(|x1BZi?e zr5dK;t4$_DJ9-ePmksF$a^vnm91DcO&htI(UT(4_zlpvW7Ud~@dQ`=eni;?wX3nxB zG0=ZD-1F}%yqPPIs|qZiy+Fk9i60IaAAvr^)O;r}{7OAicx{V={-Mni?uonb7QQUB zGv#Hxn_5?VSst^eC+p)lXFPGQdY-$`iv23VJ=~mHG2f12$a>5#1a1mGwtik|aztl~ z<(PjRxGL&ytHYd$)IEW_Vi7V~?g(5KJZL=bt%2);c9RiTuLnL9rZsQQ-_^5$A7!AL zM@s+98wYV;;8Wq&lKx{DV@stzc%V1-FY$*t*h4CjsR{foSj`7Vck@f^^`#% zkm@ye=j%twI#SyS`6J)>9TEcqDW+d@{u&=U_hCMerbW;KOKU249%jnKJaT!`njB){ z()d0(2z1)l1a;=RmLa=AFGmA$l{jcWuA#|kxVU||Fl&yV8?J^Cog8T@)yPNLNQ4H8 z>5Q8)EytT`&uk@w#q%GAc1Jga8+Hfp7zbi>nB*OLHW+4txcS^J^=x%0yeq1tuX<8T za8V$no$=3BDiKRE3UfBD;V6mYkc&RzeXI8_b7O; z3DHhLLiE!1n=4Xu+!K zIh(H2QZ=vq#YOjXUKOuSu>QZ;Dzu(G51=UB^8l7C@A_W^WdNJ@1qt|4u>5ADFT>q_ zlqQf)?*7Mv+ex$52AkW&+yjlMtae*N0GxOiosQdQ*Si5N~*MFweGdON??>Pz1^EGO4>)b z9am07cl1W~!4X{bXl2)xAhSkUM_;byQ6ritQv(FcPknZ5?R5oQaWD~+`J)kJs7NWkmHP6VxF6#D z8wPLc_iwU{ioN6@yB+d3P%~DT47iL?Z&+i*YwMEZ7^I<-_EE9NQn{5=bdwMF6)KIf zS$1ZckppOsrv)U?qz5sG?bUKX=iOE>OpZ9YA;-Gsu-PX5zQ*kZY!q_c4Ob71%r6y&AtaI`(s?=XonWL%>S>;3~N zO#gCTB@js^lIy+hUlz0&t`D#HJdQ^}>KWn8hAy{HKHRtYRdU`(?>&YFZe0$#@?pu- zfd^A8pd!R51_@tuZ<#)S2S{OCN0eN+GeEfzwf3HL1F*1N%ytL=Rbq=bM!Uy1Py&BsoXkT*rh zS^Jr`d+13j)z(?0SB|j8vjle0qcIzFk>jLYh zktz-X$$K#;erx+JK^*ubAWU}Ae<^kmqq%C^FQ#BUm$veUR)qm>s~Hh8E7{XTU=%Bnh}?F2FW{kcnBr-i&Y<*NN=B>qB`OZ bKOTZ6MYBfE-V^Re;{>|qf^}|mBq#h0jx{JP literal 0 HcmV?d00001 diff --git a/Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/SharedMediaNavigationBarArrow@2x.png b/Images.xcassets/Media Grid/TitleViewModeSelectionArrow.imageset/SharedMediaNavigationBarArrow@2x.png deleted file mode 100644 index 88beff201ede85c8bc2bc3d2d122202350e5b0cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^VjwmL8<1SPbY3!$a`$v`45_%a_RK*}1_KTjM{WP@ zE0rr(PB<@gt@-x}%fQIrExyvVC!|cG+qW~N-dSKFpux|2il6DJd!rHG3xzzV7dmz` cvz{?WzPAnUK<}D?^+tJ zgV2v$7x&3QKZ1V$5rRo5Jfgrw0~Fu^3X=%^_#*;KQ26kKQiBI5#nZ1o^ripD7SL!k zXn+QN5Bl?xh8rQM4ZaRG_eGmf`xa>*!DgHZ!QJq)a{xbd2{q4P{+(pPiICKf02>Dg zK)+Dy1SVZ_GWLYzQ3Tm=s*eaY_K;$2E#j~x6q!SqCN~UoLhcsgY~7!cSQCnEAo^EM z6gGq$>Jg(`gteL$itZr(JvJUYLhLlcoVk<_3dIkQg##gG2tm$XX#CMDG`Sc4BaeK8 zu6VxafU+I&^aA YXZJizPx&P)S5VR7efAR?kaRQ5e1V&R8(o!WI%qFoByWQ~Lt~m*GYXhM+|dh-EV|BSt|; zF50#5$IKw62nDtf18Wf#fs4pOhy-n6NISJ!g@V84&D_qJ_ntgob7#iV3&Z{H_nq%M z_vd*dg`uU>=~Fv9`mzwZMF?4^wKxHLLrSrN@h2hFRAb|rH_gq>mZ3yJxAc2QM@NsZ zt!+I(;hS3P3V+-?mQpTaB6qvGqA$GuV)h=wWHMQAS@I=_!gi9JS4vEx@T`=9Rjsp? zLgd2PthfYUD{xg#@l;dOxkNdu4Z5=-StcVtVB3zN*ou_;c03+^?#4a+4D4q^Lqj#2 zo7O|v?f6CK=DyZo0#}f$J=nj@{0g~^>+4&exiTco1%tXJ7K`DmO1Hkg)OF;I0I8Ic zJ(wgXVQh*Ko|!|ovL7<$CKAyx5Z$Al%juqEa^y5^Q&YT+*v1^R05NF-mE#YE z0`KsYe&cAWa=GspO*tTxbqwONa7+HVb#!!Ow3c630BIE9J@rEakT>)s7pAGIB8Sgt1!-p-{zRKgK)88Bz~Br_iNnBXOj^KXr$8 zsm7IUY-HXe=YoS>s;;ii70cWG9!^hB2l0s+M{>kT;5V?B3c^S2Ey4@JGsE<{xiW7I z$1`4ATXQWEiDbQg*WM+h1Icn=Abp>zk0JDxH;!_ZSCCcMmB55i^gOXnd;$Fu9(yat zY{f|T5ZZ7d_=&MqU$*&I;9^kr8zwNwGvhWcFaLT1>{T~zA}vVhCQPx)FG)l}R9FekSY2owRTQ2(!<=tJLp&_cKPA}FNfA^K7wEQO@I z&9ZrD>t7xUg$ly7$!={L@COA;#E=KKicbm(f*{)BleAKrDotCcnA+~_?p(h!vv=;y zW@hKkrb}{Rx#!+{?m6F_d+xdCEYlPl=UkteIrBoZ$@eqHcXO_7XN+ybd_VfXA?7dq zE@Q2_b?e|axm@mFMbS!ZERJh#Ztl^0_pD<8mc#FfIDT4ZOsk=*a?YlwrV78S?1|%v z1iG-WU@R}+oYXY?9SC_WPEgo7EZu~~KOPu(=&gx~iIp%O)wxihWW~F8>*qC%zuYbe z$}i%0FKC*61s0>@hV|@i=y!lW3#(^A_u=O?Sa=zB9xjy%zj%`gi8VlHXXl@;*Y5*} z_MA^v1N6C6s{j4NhY$Ygn_olD%zXBUZU6IzZS!{lJS`aTf&T&Qfl?{IDCUy%0idoW zt)C#k0|{&VsOzb*VsZSBB!VlI%ER@#ecZOSl5d}782U@aV*Ur;oD%5)b$xQ>EB|rB z3Lj=N>50+NQAn=9yP~ z<5QGPNbW!{mc3}2vu{gFsECw2bld>>Gxk3#XCc+*In-!@lG?u>Mz@Ya zE}3R!1Wx}sW;{Uv|0_=IsGCnp-YAN0H(ea^$*7OSS~Zh-azwR~SOb)l!xDS&AJdnx zYdQ#qT|(?zqEwBtAWAUs5!{Wvt2-!|10eESwhsWE+a25=93=r83Y5f^%jKt9)#Bcu z_`s+EpcX)RZ+BqT8L(^QlXTp0r!@tgD%eP%LgA@XjA3-v>g`PI8arvn*yi;*u_l) zv;$a+F=J#?0Sy7`fW-f+)lCP~2NoHefq%UZIc3uSUZz>;qTK1d00000NkvXXu0mjf DM7_68 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Share/SearchIcon.imageset/Contents.json b/Images.xcassets/Share/SearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..d5bf557e5a --- /dev/null +++ b/Images.xcassets/Share/SearchIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ShareSearchIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ShareSearchIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@2x.png b/Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..289227ed05d517509c252be414519250f38d9c43 GIT binary patch literal 1123 zcmV-p1f2VcP)Px(9Z5t%R9FekSiMgaK@@+p2r&^tL!}|cV5u=;WoK*zVyJ9R!aw0x0S7rfu<+}T zfB=m&MnM~6rIi??rNkH`JpmH|x$*a#+3}X+SZ0=myLXwL*_rq8f12%2rYstjW&l-Q6|)Pf7ReD>N5zX35*J83cm{4J?MEFq$k0=8!*9EXzfNgk~>*J?*z;+Q1TLrBlJI|6}v%6eGNxgM`BE$ha+A= zcQ#B$m&iq8f%-71++ zW6pJr35dj0&+~~VMG)XYhO}lfm>nPfli89sNmUHyHSNI|?ui+e_Dr{F8nUk0)V>sp zI}H13qlbB5;U{MgG5d<-$lz=`3?8bP(JWwh>hciEjm6n?7(5gw6Nd+T<5?II)${C8 z<8hvK7(DDVBrM>7IP%cz$Kq@{3?5PpiRGBDS9th7Dzdg%osb*``GF2RSf%@h)i{B3r23>ENhO2oGsdBh3!d^DaEzCj`@d8S>q|3nT@3r>pe@0 zA=d$RTWgUAkk$CL;q=5B>~M)TY;nrkrW|6-0_MGeEyXEYfR_hDlmziw-7HF(U1vki z!W6N9IpVzm%ND9(!C2DgIGHO^Cw0??J^0XO5LE%&OO66mu|RB>E$kk7f@VEOovw0L zvVpp>H3%)3tAHuVOAg)}kXYSr$%kCRgJDn+7N6Zlx7FVT{qNwEv;XHj2DYZFWjw5l zlv+D{SMm#fzJjCew|dL9bM*m}tNvBh6b#H~ysBD*>;}BtRSO~n^xlA}HSfzn#w^1? z%c-oO(*ZlJ_8MRup6G+IgI%{WW)A?`53mEC*M;^0K!*W#!1L%cf^HmO{?QpgHx_UP z(2WNi2)a1{2Y_xKz@DIYo+T*MNMd?snr6DLB_^9K8Eh`V1i2UJNbcjd{#kQ{MAECt pEstO)R4KXKoMVC>)wh!1{sC|mfX>2)w}1cu002ovPDHLkV1ipb4jcdg literal 0 HcmV?d00001 diff --git a/Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@3x.png b/Images.xcassets/Share/SearchIcon.imageset/ShareSearchIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..837458279a868bdc76fccbc7533bac72f406ecbc GIT binary patch literal 758 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYgdr76tf(xB_X0hW`vT{~4;lsQNzx z1Od5l29OO`0}%vr;R+xkwO}*GS-<==*_k!*=P%Lb z4a)zj{0)tcrc3_y`(R$tAgP-)hk=3dtfz}(NCo5D>8+b?8wj}O&v;^BY<&3t|ESW_ zTZJm`=j^mSJZo-plu=C5M;D{1=TeMch8PLxHMKh`R=2HY+TqnbM$>o~-7t7l*~fb3r0SwbBY$R_g(qqv6laNkUd%CFLOYv- z)A?Eq51(b)R~eyaXTHkFeEK)LAW!+d9lP}z4kNcWj^`#HiM-30!m{SCg_FvgMCKqV zGpj}c&+3YnDT{mklw3~9ZBtm9x;U6);F)YCRe7oG<)&p zUAtdclrO9hf2F_ZMEA1U;=kTpx!$oa=<8pn`&+;44ZeTyYq5*{*%0Ha#y7uTocy=u z;#=`2Rl#|db?qtFKUuR#&wnlOZZ6|@zoIgke{a9v$eO=vdD-h9mA`Fc)(N%Ov+w7W z`Df4AdHBD*Qu&YnT~GL?|L^~^Xm!*FLqul2s$qNM7B52V^d||{9aH3_H^}g JS?83{1OWRiVRrxk literal 0 HcmV?d00001 diff --git a/Images.xcassets/Share/ShareIcon.imageset/Contents.json b/Images.xcassets/Share/ShareIcon.imageset/Contents.json new file mode 100644 index 0000000000..22549bd146 --- /dev/null +++ b/Images.xcassets/Share/ShareIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ShareExternalIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ShareExternalIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@2x.png b/Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..40b4f3ac5473887eb24ec2ab856b49e459c7d144 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^Nk44ofy`glX(eb7kRokhD30_ zov}afkb%Il{oP9U+P8Yo+_m9r?yV*k(GM1Ptx7Z7w+Z#L2(3GR<%liQ5B^m)cRT}& z`fC6E+- zJKkaUzx4*&<85IR8@)xpdu}nAAkY-fZzJ<8+hDuEoOwDkDg-zWes%c4=WuPEn^EE; zhhGZ6$~d;aWt=>`<-M_M@`%UY@=0WqVw6l9cJ<{Y@Shqkf;yOnK~C@0w{kRbcn5D_ah}vcK@a z_QPrYpXvYSyfxUgJ%6V+`ym1DS%r_Uz6sjXIWuuP`${!aE~U%syH+R#F}}?1MW(`o zmG3rBbm{px{Y&cZrMAcCbwu|naVZ%ufhi7I&A~cfb=`jiT67 a^$eM<#TzA+j(q|~1%s!npUXO@geCy7-NNMn literal 0 HcmV?d00001 diff --git a/Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@3x.png b/Images.xcassets/Share/ShareIcon.imageset/ShareExternalIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..04096fb79c014b68e351d0a0c82b347d8e173883 GIT binary patch literal 558 zcmeAS@N?(olHy`uVBq!ia0vp^#z5@B!3HFwUCwF%DaPU;cPEB*=VV?2*)Kd@978f# z-_COMJ8U4}`gYpw16`93bg`OiI%zHXqNWp3C}LJH=hP9-X)(#^M!m;{UGE1?J(hg_ z$KUtIvO<$D%;2)w$^JP0&{?kM=C&=y?`vhV<<8FjJ7JY3_v5<~4%_M()Rj3@*gwmw zNGP$XFbA^Yb~D-=Mnpzm(0F& zGMsIfWIj1pbDuv)h8&N$$_^El63a99?y9gaihuRRIEClM%~aklcdPTUiwd(3H!YAMqh(l>3=S@3yPyz{Rk4|UGWTK}&4uWq_m z;hS@{WgB-V&EZIxEy%R8HT>zB4>LWtir6f@9F+duy}G{4|E|jXuc4OA5~pg{TFki^ zG;bAmx<>1kM+I))m8B?ONZ9yK|eCb=JEPr)K(n}Fjao#_1-z+ip zdmpX`<{0-G>K;Edb6Qq$)DBSFIr)ECHqiLG{(SH5 V^S|yUu>cbQgQu&X%Q~loCII|v^sWE^ literal 0 HcmV?d00001 diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 89ae77074f..5255da681f 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ D00ADFDB1EBA2EAF00873D2E /* OngoingCallContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDA1EBA2EAF00873D2E /* OngoingCallContext.swift */; }; D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */; }; D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00BDA1E1EE5B69200C64C5E /* ChannelAdminController.swift */; }; + D00BED201F73F60F00922292 /* ShareSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00BED1F1F73F60F00922292 /* ShareSearchContainerNode.swift */; }; + D00BED221F73F82400922292 /* SharePeersContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00BED211F73F82400922292 /* SharePeersContainerNode.swift */; }; D00FF2091F4E2414006FA332 /* InstantPageSettingsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00FF2081F4E2414006FA332 /* InstantPageSettingsNode.swift */; }; D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */; }; D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */; }; @@ -33,6 +35,8 @@ D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */; }; D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */; }; D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */; }; + D025A4231F79344500563950 /* FetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4221F79344500563950 /* FetchManager.swift */; }; + D025A4261F79428E00563950 /* FetchManagerLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4251F79428E00563950 /* FetchManagerLocation.swift */; }; D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */; }; D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B481EFD59170074D609 /* BotCheckoutControllerNode.swift */; }; @@ -66,9 +70,15 @@ D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */; }; D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */; }; D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */; }; + D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = D064EF861F69A06F00AC0398 /* MessageContentKind.swift */; }; + D0684A041F6C3AD50059F570 /* ChatListTypingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0684A031F6C3AD50059F570 /* ChatListTypingNode.swift */; }; + D06887F01F72DEE6000AB936 /* ShareInputFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06887EF1F72DEE6000AB936 /* ShareInputFieldNode.swift */; }; D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */; }; D06BEC771F62F68B0035A545 /* OverlayUniversalVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */; }; D06BEC8A1F6597A80035A545 /* OverlayVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */; }; + D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */; }; + D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */; }; + D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */; }; D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */; }; D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */; }; D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */; }; @@ -81,6 +91,10 @@ D079FCE91F06A76C0038FADE /* Notices.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCE81F06A76C0038FADE /* Notices.swift */; }; D07BCBFE1F2B792300ED97AA /* LegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D07BCBFD1F2B792300ED97AA /* LegacyComponents.framework */; }; D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; }; + D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */; }; + D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */; }; + D087BFB11F745483003FD209 /* ShareSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */; }; + D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */; }; D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC40821D5B8E7400261D9D /* TelegramUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */; }; D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74C1EEFEE1500A3128C /* GameController.swift */; }; @@ -93,8 +107,8 @@ D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */; }; D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63AF1F1010FE003444CD /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63B11F11289A003444CD /* PassKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - D0A8BB9F1F61EC9D000F03FD /* ChatTextInputPanelNodeOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8BB9E1F61EC9D000F03FD /* ChatTextInputPanelNodeOperators.swift */; }; D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8BBA01F61EE83000F03FD /* UniversalVideoCalleryItem.swift */; }; + D0AA29AE1F72770D00C050AC /* ChatListItemStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AA29AD1F72770D00C050AC /* ChatListItemStrings.swift */; }; D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB191EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift */; }; D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */; }; D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; }; @@ -104,6 +118,7 @@ D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; }; D0B4AF881EC112EE00D51FF6 /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B4AF871EC112ED00D51FF6 /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */; }; + D0BDB09B1F79C658002ABF2F /* SaveToCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */; }; D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */; }; D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */; }; D0C0B59B1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B59A1EE019E5000F4D2C /* ChatSearchNavigationContentNode.swift */; }; @@ -113,6 +128,10 @@ D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */; }; D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */; }; D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */; }; + D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */; }; + D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */; }; + D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */; }; + D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */; }; D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; }; D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; }; D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */; }; @@ -236,6 +255,7 @@ D0EB42001F30ED4F00838FE6 /* LegacyImageProcessors.h in Headers */ = {isa = PBXBuildFile; fileRef = D0EB41FE1F30ED4F00838FE6 /* LegacyImageProcessors.h */; }; D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */ = {isa = PBXBuildFile; fileRef = D0EB41FF1F30ED4F00838FE6 /* LegacyImageProcessors.m */; }; D0EB42051F3143AB00838FE6 /* LegacyComponentsResources.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D0EB42041F3143AB00838FE6 /* LegacyComponentsResources.bundle */; }; + D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */; }; D0EC6C541EB9F42E00EBF1C3 /* checks.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6B9E1EB9F42D00EBF1C3 /* checks.cc */; }; D0EC6C551EB9F42E00EBF1C3 /* stringutils.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6BA51EB9F42D00EBF1C3 /* stringutils.cc */; }; D0EC6C561EB9F42E00EBF1C3 /* audio_util.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0EC6BA91EB9F42D00EBF1C3 /* audio_util.cc */; }; @@ -631,7 +651,6 @@ D0EC6DF41EB9F58900EBF1C3 /* ShareActionButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00DE6AC1E8EB2D4003F0D76 /* ShareActionButtonNode.swift */; }; D0EC6DF51EB9F58900EBF1C3 /* PeerMediaCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */; }; D0EC6DF61EB9F58900EBF1C3 /* PeerMediaCollectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */; }; - D0EC6DF71EB9F58900EBF1C3 /* PeerMediaCollectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE76FD1D92EFF2002B8809 /* PeerMediaCollectionTitleView.swift */; }; D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */; }; D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */; }; D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE772A1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift */; }; @@ -904,6 +923,7 @@ D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; }; D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; + D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */; }; D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */; }; D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */; }; D0FE4DE41F0AEBB900E8A0B3 /* SharedVideoContextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */; }; @@ -932,6 +952,8 @@ D00B3F9F1E3A76D4003872C3 /* ItemListSwitchItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListSwitchItem.swift; sourceTree = ""; }; D00B3FA11E3A983E003872C3 /* ItemListTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextItem.swift; sourceTree = ""; }; D00BDA1E1EE5B69200C64C5E /* ChannelAdminController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminController.swift; sourceTree = ""; }; + D00BED1F1F73F60F00922292 /* ShareSearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSearchContainerNode.swift; sourceTree = ""; }; + D00BED211F73F82400922292 /* SharePeersContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePeersContainerNode.swift; sourceTree = ""; }; D00C7CD61E3664070080C3D5 /* ItemListMultilineInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListMultilineInputItem.swift; sourceTree = ""; }; D00C7CD81E36B2DB0080C3D5 /* ContactListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactListNode.swift; sourceTree = ""; }; D00C7CDB1E3776E50080C3D5 /* SecretMediaPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretMediaPreviewController.swift; sourceTree = ""; }; @@ -1039,6 +1061,8 @@ D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyAttachmentMenu.swift; sourceTree = ""; }; D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyEmptyController.swift; sourceTree = ""; }; D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyNavigationController.swift; sourceTree = ""; }; + D025A4221F79344500563950 /* FetchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchManager.swift; sourceTree = ""; }; + D025A4251F79428E00563950 /* FetchManagerLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchManagerLocation.swift; sourceTree = ""; }; D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LegacyLocationVenueIconDataSource.swift; path = "../third-party/RMIntro/LegacyLocationVenueIconDataSource.swift"; sourceTree = ""; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; @@ -1215,12 +1239,18 @@ D0613FCC1E60482300202CDB /* ChannelMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersController.swift; sourceTree = ""; }; D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = ""; }; D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtons.swift; sourceTree = ""; }; + D064EF861F69A06F00AC0398 /* MessageContentKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentKind.swift; sourceTree = ""; }; + D0684A031F6C3AD50059F570 /* ChatListTypingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTypingNode.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; + D06887EF1F72DEE6000AB936 /* ShareInputFieldNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareInputFieldNode.swift; sourceTree = ""; }; D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyInstantVideoController.swift; sourceTree = ""; }; D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayUniversalVideoNode.swift; sourceTree = ""; }; D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayVideoDecoration.swift; sourceTree = ""; }; + D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEmbedVideoContent.swift; sourceTree = ""; }; + D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLoadingNode.swift; sourceTree = ""; }; D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = ""; }; + D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = ""; }; D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeEssentialGraphics.swift; sourceTree = ""; }; D06FFBA91EAFAD2500CB53D4 /* PresentationResourcesChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChat.swift; sourceTree = ""; }; D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioPlaylistPlayer.swift; sourceTree = ""; }; @@ -1273,6 +1303,10 @@ D087751B1E3F542500A97350 /* ContactMultiselectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactMultiselectionControllerNode.swift; sourceTree = ""; }; D087751D1E3F579300A97350 /* CounterContollerTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterContollerTitleView.swift; sourceTree = ""; }; D087751F1E3F595000A97350 /* ContactListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactListActionItem.swift; sourceTree = ""; }; + D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContentContainerNode.swift; sourceTree = ""; }; + D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLoadingContainerNode.swift; sourceTree = ""; }; + D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSearchBarNode.swift; sourceTree = ""; }; + D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareControllerRecentPeersGridItem.swift; sourceTree = ""; }; D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePresentationSettings.swift; sourceTree = ""; }; D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = ""; }; D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = ""; }; @@ -1307,8 +1341,8 @@ D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = ""; }; D0A11BFD1E7840A50081CE03 /* ChangePhoneNumberControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberControllerNode.swift; sourceTree = ""; }; D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = ""; }; - D0A8BB9E1F61EC9D000F03FD /* ChatTextInputPanelNodeOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelNodeOperators.swift; sourceTree = ""; }; D0A8BBA01F61EE83000F03FD /* UniversalVideoCalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalVideoCalleryItem.swift; sourceTree = ""; }; + D0AA29AD1F72770D00C050AC /* ChatListItemStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListItemStrings.swift; sourceTree = ""; }; D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; D0AB0BB21D6718EB002C78E7 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; @@ -1340,6 +1374,7 @@ D0BC38691E3FB94D0044D6FE /* CreateGroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroupController.swift; sourceTree = ""; }; D0BC387E1E40F1CF0044D6FE /* ContactSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionController.swift; sourceTree = ""; }; D0BC38801E40F1D80044D6FE /* ContactSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSelectionControllerNode.swift; sourceTree = ""; }; + D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToCameraRoll.swift; sourceTree = ""; }; D0BE383B1E7C3E51000079AF /* StickerPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewController.swift; sourceTree = ""; }; D0BE931A1E92DFBA00DCC1E6 /* StickerPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewControllerNode.swift; sourceTree = ""; }; D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; @@ -1364,6 +1399,9 @@ D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBotInfoItem.swift; sourceTree = ""; }; D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = ""; }; D0CE1BD21E51BC6100404327 /* DebugController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugController.swift; sourceTree = ""; }; + D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputAccessoryItem.swift; sourceTree = ""; }; + D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelState.swift; sourceTree = ""; }; + D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformImageArguments.swift; sourceTree = ""; }; D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = ""; }; D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; @@ -1410,7 +1448,6 @@ D0DC35491DE366CD000195EB /* CommandChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandChatInputContextPanelNode.swift; sourceTree = ""; }; D0DC354B1DE366DE000195EB /* CommandChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandChatInputPanelItem.swift; sourceTree = ""; }; D0DE76F61D91BA3D002B8809 /* GridHoleItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridHoleItem.swift; sourceTree = ""; }; - D0DE76FD1D92EFF2002B8809 /* PeerMediaCollectionTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionTitleView.swift; sourceTree = ""; }; D0DE76FF1D92F1EB002B8809 /* ChatTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTitleView.swift; sourceTree = ""; }; D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionInterfaceState.swift; sourceTree = ""; }; D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionInterfaceStateButtons.swift; sourceTree = ""; }; @@ -1428,6 +1465,7 @@ D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = ""; }; D0E23DD71E805E2600B9B6D2 /* FeaturedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeaturedStickerPacksController.swift; sourceTree = ""; }; D0E23DDC1E8081A200B9B6D2 /* ArhivedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArhivedStickerPacksController.swift; sourceTree = ""; }; + D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleVideoDecoration.swift; sourceTree = ""; }; D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidateAddressNameInteractive.swift; sourceTree = ""; }; D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerEmptyStateItem.swift; sourceTree = ""; }; D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListLoadingIndicatorEmptyStateItem.swift; sourceTree = ""; }; @@ -1563,6 +1601,7 @@ D0EB41FE1F30ED4F00838FE6 /* LegacyImageProcessors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LegacyImageProcessors.h; sourceTree = ""; }; D0EB41FF1F30ED4F00838FE6 /* LegacyImageProcessors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LegacyImageProcessors.m; sourceTree = ""; }; D0EB42041F3143AB00838FE6 /* LegacyComponentsResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = LegacyComponentsResources.bundle; path = ../LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle; sourceTree = ""; }; + D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionEmptyNode.swift; sourceTree = ""; }; D0EC6B351EB88D0A00EBF1C3 /* ThemeGridController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridController.swift; sourceTree = ""; }; D0EC6B371EB88D1600EBF1C3 /* ThemeGridControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerNode.swift; sourceTree = ""; }; D0EC6B3A1EB8CF2B00EBF1C3 /* CallController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; @@ -2034,6 +2073,7 @@ D0FC40881D5B8E7500261D9D /* TelegramUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TelegramUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelegramUITests.swift; sourceTree = ""; }; D0FC408F1D5B8E7500261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePeerNode.swift; sourceTree = ""; }; D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationSurfaceLevels.swift; sourceTree = ""; }; D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantVideoNode.swift; sourceTree = ""; }; D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedVideoContextManager.swift; sourceTree = ""; }; @@ -2098,7 +2138,14 @@ D00DE6971E8E8E33003F0D76 /* ShareController.swift */, D00DE6991E8E8E43003F0D76 /* ShareControllerNode.swift */, D00DE69B1E8E8E97003F0D76 /* ShareControllerPeerGridItem.swift */, + D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */, D00DE6AC1E8EB2D4003F0D76 /* ShareActionButtonNode.swift */, + D06887EF1F72DEE6000AB936 /* ShareInputFieldNode.swift */, + D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */, + D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */, + D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */, + D00BED211F73F82400922292 /* SharePeersContainerNode.swift */, + D00BED1F1F73F60F00922292 /* ShareSearchContainerNode.swift */, ); name = Share; sourceTree = ""; @@ -2208,6 +2255,15 @@ name = Media; sourceTree = ""; }; + D025A4241F79428300563950 /* Fetch Manager */ = { + isa = PBXGroup; + children = ( + D025A4251F79428E00563950 /* FetchManagerLocation.swift */, + D025A4221F79344500563950 /* FetchManager.swift */, + ); + name = "Fetch Manager"; + sourceTree = ""; + }; D02BE0751D9190CD000889C2 /* Grid Items */ = { isa = PBXGroup; children = ( @@ -2273,8 +2329,10 @@ D0477D1A1F617E5800412B44 /* UniversalVideoNode.swift */, D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */, D0477D1C1F617E8900412B44 /* NativeVideoContent.swift */, + D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */, D0477D1E1F619E0700412B44 /* GalleryVideoDecoration.swift */, D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */, + D0E266FC1F66706500BFC79F /* ChatBubbleVideoDecoration.swift */, D0477D201F61A47600412B44 /* UniversalVideoContentManager.swift */, ); name = Video; @@ -2641,10 +2699,12 @@ D07CFF781DCA226F00761F81 /* ChatListNode.swift */, D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */, D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */, + D0AA29AD1F72770D00C050AC /* ChatListItemStrings.swift */, D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */, D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */, D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */, D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */, + D0684A031F6C3AD50059F570 /* ChatListTypingNode.swift */, ); name = "Chat List Node"; sourceTree = ""; @@ -2813,11 +2873,11 @@ children = ( D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */, D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */, - D0DE76FD1D92EFF2002B8809 /* PeerMediaCollectionTitleView.swift */, D0DE77221D932043002B8809 /* PeerMediaCollectionInterfaceState.swift */, D0DE77241D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift */, D0DE772A1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift */, D01776BD1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift */, + D0EB5ADE1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift */, ); name = "Peer Media Collection"; sourceTree = ""; @@ -2898,6 +2958,15 @@ name = "Data and Storage"; sourceTree = ""; }; + D0CE8CEA1F6FCC8200AA2DB0 /* Transform Image */ = { + isa = PBXGroup; + children = ( + D0F69DC61D6B89E70046BCD6 /* TransformImageNode.swift */, + D0CE8CEB1F6FCCA300AA2DB0 /* TransformImageArguments.swift */, + ); + name = "Transform Image"; + sourceTree = ""; + }; D0D03AE61DECB0D200220C46 /* Audio Recorder */ = { isa = PBXGroup; children = ( @@ -3116,6 +3185,7 @@ D0DE77261D932627002B8809 /* ChatHistoryNode.swift */, D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */, D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */, + D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */, D00D34361E6E14E30057B307 /* ChatMessageThrottledProcessingManager.swift */, D0DE772C1D934DCB002B8809 /* List Items */, D02BE0751D9190CD000889C2 /* Grid Items */, @@ -4054,8 +4124,8 @@ D01776B61F1D6CCF0044446D /* Radial Status */, D0F69DCA1D6B89F20046BCD6 /* Search */, D0477D191F617E4B00412B44 /* Video */, + D0CE8CEA1F6FCC8200AA2DB0 /* Transform Image */, D0F69DC81D6B89EB0046BCD6 /* ImageNode.swift */, - D0F69DC61D6B89E70046BCD6 /* TransformImageNode.swift */, D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */, D00C7CE51E378FD00080C3D5 /* RadialTimeoutNode.swift */, D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */, @@ -4068,6 +4138,7 @@ D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */, D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */, D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */, + D0FC4FBA1F751E8900B7443F /* SelectablePeerNode.swift */, ); name = Nodes; sourceTree = ""; @@ -4179,6 +4250,7 @@ D0EE97191D88BCA0006C18E1 /* ChatInfo.swift */, D0EF40DE1E73100D000DFCD4 /* ChatHistoryNavigationStack.swift */, D01C2AA01E758F90001F6F9A /* NavigateToChatController.swift */, + D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */, D0F69E181D6B8AD10046BCD6 /* Items */, D03ADB461D703250005A521C /* Interface State */, D03ADB491D704427005A521C /* Accessory Panels */, @@ -4237,11 +4309,12 @@ isa = PBXGroup; children = ( D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */, - D0A8BB9E1F61EC9D000F03FD /* ChatTextInputPanelNodeOperators.swift */, D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */, D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */, D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */, D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */, + D0CE8CE41F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift */, + D0CE8CE61F6F35A300AA2DB0 /* ChatTextInputPanelState.swift */, ); name = "Text Input"; sourceTree = ""; @@ -4382,6 +4455,7 @@ D0F69E911D6B8C8E0046BCD6 /* Utils */ = { isa = PBXGroup; children = ( + D025A4241F79428300563950 /* Fetch Manager */, D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, @@ -4410,6 +4484,8 @@ D079FCDC1F05C4F20038FADE /* LocalAuth.swift */, D079FCE81F06A76C0038FADE /* Notices.swift */, D0FE4DDB1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift */, + D064EF861F69A06F00AC0398 /* MessageContentKind.swift */, + D0BDB09A1F79C658002ABF2F /* SaveToCameraRoll.swift */, ); name = Utils; sourceTree = ""; @@ -4715,6 +4791,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0684A041F6C3AD50059F570 /* ChatListTypingNode.swift in Sources */, D0EC6CAE1EB9F58800EBF1C3 /* animations.c in Sources */, D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */, D0EC6CAF1EB9F58800EBF1C3 /* buffer.c in Sources */, @@ -4782,6 +4859,7 @@ D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */, D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */, D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */, + D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */, D0EC6FE31EBA135100EBF1C3 /* spl_sqrt.c in Sources */, D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */, D0EC6CD21EB9F58800EBF1C3 /* HapticFeedback.swift in Sources */, @@ -4853,6 +4931,7 @@ D0EC6CF91EB9F58800EBF1C3 /* MediaManager.swift in Sources */, D01776B81F1D6FB30044446D /* RadialProgressContentNode.swift in Sources */, D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */, + D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */, D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */, D0EC6CFC1EB9F58800EBF1C3 /* ManagedAudioPlaylistPlayer.swift in Sources */, D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */, @@ -4941,11 +5020,14 @@ D0EC6D311EB9F58800EBF1C3 /* RadialTimeoutNode.swift in Sources */, D0EC6D321EB9F58800EBF1C3 /* TextNode.swift in Sources */, D0EC6D331EB9F58800EBF1C3 /* ListSectionHeaderNode.swift in Sources */, + D0BDB09B1F79C658002ABF2F /* SaveToCameraRoll.swift in Sources */, + D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */, D0EC6D341EB9F58800EBF1C3 /* AvatarNode.swift in Sources */, D0EC6D351EB9F58800EBF1C3 /* SearchBarNode.swift in Sources */, D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, D0EC6D371EB9F58800EBF1C3 /* SearchDisplayController.swift in Sources */, D0EC6D381EB9F58800EBF1C3 /* SearchDisplayControllerContentNode.swift in Sources */, + D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */, D0EC6D391EB9F58800EBF1C3 /* ImageContainingNode.swift in Sources */, D0EC6FB61EBA114200EBF1C3 /* aecm_core.cc in Sources */, D0EC6D3A1EB9F58800EBF1C3 /* AudioWaveformNode.swift in Sources */, @@ -4957,9 +5039,12 @@ D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */, D0EC6D3E1EB9F58800EBF1C3 /* TelegramController.swift in Sources */, D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */, + D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */, D0EC6D3F1EB9F58800EBF1C3 /* MediaNavigationAccessoryPanel.swift in Sources */, D0E9BA3B1F0558E800F079A4 /* NSString+Stripe.m in Sources */, + D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */, D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */, + D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */, D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */, D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */, D0EC6D421EB9F58800EBF1C3 /* MediaNavigationAccessoryItemListNode.swift in Sources */, @@ -4981,6 +5066,7 @@ D0EC6D571EB9F58800EBF1C3 /* ChatHistoryListNode.swift in Sources */, D0EC6D581EB9F58800EBF1C3 /* ChatHistoryGridNode.swift in Sources */, D0EC6D591EB9F58800EBF1C3 /* ChatMessageThrottledProcessingManager.swift in Sources */, + D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */, D0EC6D5A1EB9F58800EBF1C3 /* ListMessageItem.swift in Sources */, D0EC6D5B1EB9F58800EBF1C3 /* ListMessageNode.swift in Sources */, D0EC6D5C1EB9F58800EBF1C3 /* ListMessageFileItemNode.swift in Sources */, @@ -5001,6 +5087,8 @@ D0EC6D631EB9F58800EBF1C3 /* ContactListActionItem.swift in Sources */, D0EC6D641EB9F58800EBF1C3 /* ContactsPeerItem.swift in Sources */, D0EC6D651EB9F58800EBF1C3 /* ContactsVCardItem.swift in Sources */, + D00BED201F73F60F00922292 /* ShareSearchContainerNode.swift in Sources */, + D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */, D0EC6D661EB9F58800EBF1C3 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, D0EC6D671EB9F58800EBF1C3 /* ContactListNameIndexHeader.swift in Sources */, D0EC6D681EB9F58800EBF1C3 /* AuthorizationSequenceController.swift in Sources */, @@ -5102,6 +5190,7 @@ D0EC6EB81EBA0FD000EBF1C3 /* AudioOutputAudioUnit.cpp in Sources */, D0EC6DA61EB9F58900EBF1C3 /* ChatEmptyItem.swift in Sources */, D0E9BAE41F0574D800F079A4 /* STPBankAccountParams.m in Sources */, + D06887F01F72DEE6000AB936 /* ShareInputFieldNode.swift in Sources */, D0EC6DA71EB9F58900EBF1C3 /* ChatMessageBackground.swift in Sources */, D0F0AAE01EC1E12C005EE2A5 /* PresentationCall.swift in Sources */, D0EC6DA81EB9F58900EBF1C3 /* ChatInterfaceState.swift in Sources */, @@ -5172,6 +5261,7 @@ D0EC6DD61EB9F58900EBF1C3 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */, D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, + D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */, D0EC6DD91EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */, D0E9BA161F05574500F079A4 /* STPCardValidator.m in Sources */, D0EC6DDA1EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */, @@ -5213,18 +5303,17 @@ D0EC6FC21EBA135100EBF1C3 /* auto_corr_to_refl_coef.c in Sources */, D0EC6DF21EB9F58900EBF1C3 /* ShareControllerNode.swift in Sources */, D0EC6DF31EB9F58900EBF1C3 /* ShareControllerPeerGridItem.swift in Sources */, - D0A8BB9F1F61EC9D000F03FD /* ChatTextInputPanelNodeOperators.swift in Sources */, D0EC6DF41EB9F58900EBF1C3 /* ShareActionButtonNode.swift in Sources */, D0EC6DF51EB9F58900EBF1C3 /* PeerMediaCollectionController.swift in Sources */, D0EC6DF61EB9F58900EBF1C3 /* PeerMediaCollectionControllerNode.swift in Sources */, D0EC6FCA1EBA135100EBF1C3 /* division_operations.c in Sources */, - D0EC6DF71EB9F58900EBF1C3 /* PeerMediaCollectionTitleView.swift in Sources */, D0477D1D1F617E8900412B44 /* NativeVideoContent.swift in Sources */, D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */, D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */, D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */, D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, + D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */, D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */, D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */, D0EC6DFD1EB9F58900EBF1C3 /* GalleryControllerNode.swift in Sources */, @@ -5306,7 +5395,9 @@ D0EC6E311EB9F58900EBF1C3 /* ContactSelectionControllerNode.swift in Sources */, D0EC6EAB1EBA0FBB00EBF1C3 /* CongestionControl.cpp in Sources */, D0EC6E321EB9F58900EBF1C3 /* CreateGroupController.swift in Sources */, + D00BED221F73F82400922292 /* SharePeersContainerNode.swift in Sources */, D0EC6E331EB9F58900EBF1C3 /* CreateChannelController.swift in Sources */, + D0AA29AE1F72770D00C050AC /* ChatListItemStrings.swift in Sources */, D0EC6E341EB9F58900EBF1C3 /* ItemListItem.swift in Sources */, D0EC6E351EB9F58900EBF1C3 /* ItemListAvatarAndNameItem.swift in Sources */, D0EC6E361EB9F58900EBF1C3 /* ItemListTextWithLabelItem.swift in Sources */, @@ -5319,6 +5410,7 @@ D0EC6E3B1EB9F58900EBF1C3 /* ItemListPeerItem.swift in Sources */, D0EC6E3C1EB9F58900EBF1C3 /* ItemListPeerActionItem.swift in Sources */, D0EC6E3D1EB9F58900EBF1C3 /* ItemListMultilineInputItem.swift in Sources */, + D0CE8CE71F6F35A300AA2DB0 /* ChatTextInputPanelState.swift in Sources */, D0EC6E3E1EB9F58900EBF1C3 /* ItemListSectionHeaderItem.swift in Sources */, D0EC6E3F1EB9F58900EBF1C3 /* ItemListTextItem.swift in Sources */, D0EC6E401EB9F58900EBF1C3 /* ItemListActivityTextItem.swift in Sources */, @@ -5340,6 +5432,7 @@ D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */, D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */, D0EC6E4F1EB9F58900EBF1C3 /* ChannelVisibilityController.swift in Sources */, + D025A4231F79344500563950 /* FetchManager.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, D0EC6E511EB9F58900EBF1C3 /* ChannelBlacklistController.swift in Sources */, @@ -5363,6 +5456,7 @@ D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */, D0EC6E5F1EB9F58900EBF1C3 /* RecentSessionsController.swift in Sources */, D0EC6E601EB9F58900EBF1C3 /* BlockedPeersController.swift in Sources */, + D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */, D0EC6E611EB9F58900EBF1C3 /* SelectivePrivacySettingsController.swift in Sources */, D0471B4B1EFD64AC0074D609 /* BotCheckoutHeaderItem.swift in Sources */, D0EC6E621EB9F58900EBF1C3 /* SelectivePrivacySettingsPeersController.swift in Sources */, @@ -5392,6 +5486,8 @@ D0EC6E721EB9F58900EBF1C3 /* ThemeGalleryItem.swift in Sources */, D0471B581EFE6D020074D609 /* BotCheckoutInfoController.swift in Sources */, D0EC6E731EB9F58900EBF1C3 /* ThemeGalleryToolbarNode.swift in Sources */, + D025A4261F79428E00563950 /* FetchManagerLocation.swift in Sources */, + D087BFB11F745483003FD209 /* ShareSearchBarNode.swift in Sources */, D0EC6FC41EBA135100EBF1C3 /* complex_bit_reverse.c in Sources */, D0EC6E741EB9F58900EBF1C3 /* ThemeGridController.swift in Sources */, D0EC6E751EB9F58900EBF1C3 /* ThemeGridControllerNode.swift in Sources */, diff --git a/TelegramUI/ActivityIndicator.swift b/TelegramUI/ActivityIndicator.swift index eab0667b27..f78e27c74c 100644 --- a/TelegramUI/ActivityIndicator.swift +++ b/TelegramUI/ActivityIndicator.swift @@ -57,7 +57,7 @@ final class ActivityIndicator: ASDisplayNode { case let .navigationAccent(theme): self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) case let .custom(color): - self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color) + self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: 22.0) } super.init() diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index f7404dc443..dc936f6873 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -7,28 +7,37 @@ import AsyncDisplayKit import TelegramCore enum AvatarGalleryEntry: Equatable { - case topImage([TelegramMediaImageRepresentation]) - case image(TelegramMediaImage) + case topImage([TelegramMediaImageRepresentation], GalleryItemIndexData?) + case image(TelegramMediaImage, GalleryItemIndexData?) var representations: [TelegramMediaImageRepresentation] { switch self { - case let .topImage(representations): + case let .topImage(representations, _): return representations - case let.image(image): + case let .image(image, _): return image.representations } } + var indexData: GalleryItemIndexData? { + switch self { + case let .topImage(_, indexData): + return indexData + case let .image(_, indexData): + return indexData + } + } + static func ==(lhs: AvatarGalleryEntry, rhs: AvatarGalleryEntry) -> Bool { switch lhs { - case let .topImage(lhsRepresentations): - if case let .topImage(rhsRepresentations) = rhs, lhsRepresentations == rhsRepresentations { + case let .topImage(lhsRepresentations, lhsIndexData): + if case let .topImage(rhsRepresentations, rhsIndexData) = rhs, lhsRepresentations == rhsRepresentations, lhsIndexData == rhsIndexData { return true } else { return false } - case let .image(lhsImage): - if case let .image(rhsImage) = rhs, lhsImage.isEqual(rhsImage) { + case let .image(lhsImage, lhsIndexData): + if case let .image(rhsImage, rhsIndexData) = rhs, lhsImage.isEqual(rhsImage), lhsIndexData == rhsIndexData { return true } else { return false @@ -45,6 +54,41 @@ final class AvatarGalleryControllerPresentationArguments { } } +private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry]{ + var initialEntries: [AvatarGalleryEntry] = [] + if let user = peer as? TelegramUser, !user.photo.isEmpty { + initialEntries.append(.topImage(user.photo, nil)) + } else if let group = peer as? TelegramGroup { + initialEntries.append(.topImage(group.photo, nil)) + } else if let channel = peer as? TelegramChannel { + initialEntries.append(.topImage(channel.photo, nil)) + } + return initialEntries +} + +func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { + return requestPeerPhotos(account: account, peerId: peer.id) |> map { photos -> [AvatarGalleryEntry] in + var result: [AvatarGalleryEntry] = [] + let initialEntries = initialAvatarGalleryEntries(peer: peer) + if photos.isEmpty { + result = initialEntries + } else { + var index: Int32 = 0 + for photo in photos { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) + if result.isEmpty, let first = initialEntries.first { + let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations) + result.append(.image(image, indexData)) + } else { + result.append(.image(photo.image, indexData)) + } + index += 1 + } + } + return result + } +} + class AvatarGalleryController: ViewController { private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -52,6 +96,8 @@ class AvatarGalleryController: ViewController { private let account: Account + private var presentationData: PresentationData + private let _ready = Promise() override var ready: Promise { return self._ready @@ -76,50 +122,33 @@ class AvatarGalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void - init(account: Account, peer: Peer, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { + init(account: Account, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.statusBar.statusBarStyle = .White - var initialEntries: [AvatarGalleryEntry] = [] - if let user = peer as? TelegramUser, !user.photo.isEmpty { - initialEntries.append(.topImage(user.photo)) - } else if let group = peer as? TelegramGroup { - initialEntries.append(.topImage(group.photo)) - } else if let channel = peer as? TelegramChannel { - initialEntries.append(.topImage(channel.photo)) + let remoteEntriesSignal: Signal<[AvatarGalleryEntry], NoError> + if let remoteEntries = remoteEntries { + remoteEntriesSignal = remoteEntries.get() + } else { + remoteEntriesSignal = fetchedAvatarGalleryEntries(account: account, peer: peer) } - let remoteEntriesSignal: Signal<[AvatarGalleryEntry], NoError> = requestPeerPhotos(account: account, peerId: peer.id) |> map { photos -> [AvatarGalleryEntry] in - var result: [AvatarGalleryEntry] = [] - if photos.isEmpty { - result = initialEntries - } else { - for photo in photos { - if result.isEmpty, let first = initialEntries.first { - let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations) - result.append(.image(image)) - } else { - result.append(.image(photo.image)) - } - } - } - return result - } - - let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = .single(initialEntries) |> then(remoteEntriesSignal) + let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = .single(initialAvatarGalleryEntries(peer: peer)) |> then(remoteEntriesSignal) + let presentationData = self.presentationData self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in if let strongSelf = self { strongSelf.entries = entries strongSelf.centralEntryIndex = 0 if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ PeerAvatarImageGalleryItem(account: account, entry: $0) }), centralItemIndex: 0, keepFirst: true) + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ PeerAvatarImageGalleryItem(account: account, strings: presentationData.strings, entry: $0) }), centralItemIndex: 0, keepFirst: true) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in strongSelf?.didSetReady = true @@ -130,7 +159,9 @@ class AvatarGalleryController: ViewController { })) self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in - self?.navigationItem.title = title + if let strongSelf = self { + strongSelf.navigationItem.setTitle(title, animated: strongSelf.navigationItem.title?.isEmpty ?? true) + } })) self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in @@ -207,6 +238,9 @@ class AvatarGalleryController: ViewController { self.galleryNode.transitionNodeForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? AvatarGalleryControllerPresentationArguments { + if centralItemNode.index != 0 { + return nil + } if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { return transitionArguments.transitionNode } @@ -219,7 +253,8 @@ class AvatarGalleryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) } - self.galleryNode.pager.replaceItems(self.entries.map({ PeerAvatarImageGalleryItem(account: self.account, entry: $0) }), centralItemIndex: self.centralEntryIndex) + let presentationData = self.presentationData + self.galleryNode.pager.replaceItems(self.entries.map({ PeerAvatarImageGalleryItem(account: self.account, strings: presentationData.strings, entry: $0) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 59b9e3d647..e30bb3f9e3 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -28,7 +28,7 @@ private let gradientColors: [NSArray] = [ [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] + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private let grayscaleColors: NSArray = [ diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index 5fd52afd96..0e67df6f30 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -92,6 +92,7 @@ public final class CallListController: ViewController { } self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 383c6499b0..a362914079 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -14,6 +14,7 @@ private final class ChannelInfoControllerArguments { let updateEditingDescriptionText: (String) -> Void let openChannelTypeSetup: () -> Void let changeNotificationMuteSettings: () -> Void + let changeNotificationSoundSettings: () -> Void let openSharedMedia: () -> Void let openAdmins: () -> Void let openMembers: () -> Void @@ -23,7 +24,7 @@ private final class ChannelInfoControllerArguments { let deleteChannel: () -> Void let displayAddressNameContextMenu: (String) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.tapAvatarAction = tapAvatarAction @@ -32,6 +33,7 @@ private final class ChannelInfoControllerArguments { self.updateEditingDescriptionText = updateEditingDescriptionText self.openChannelTypeSetup = openChannelTypeSetup self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.changeNotificationSoundSettings = changeNotificationSoundSettings self.openSharedMedia = openSharedMedia self.openAdmins = openAdmins self.openMembers = openMembers @@ -65,6 +67,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry { case members(theme: PresentationTheme, text: String, value: String) case banned(theme: PresentationTheme, text: String, value: String) case notifications(theme: PresentationTheme, text: String, value: String) + case notificationSound(theme: PresentationTheme, text: String, value: String) case sharedMedia(theme: PresentationTheme, text: String) case report(theme: PresentationTheme, text: String) case leave(theme: PresentationTheme, text: String) @@ -76,7 +79,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry { return ChannelInfoSection.info.rawValue case .admins, .members, .banned: return ChannelInfoSection.members.rawValue - case .sharedMedia, .notifications: + case .sharedMedia, .notifications, .notificationSound: return ChannelInfoSection.sharedMediaAndNotifications.rawValue case .report, .leave, .deleteChannel: return ChannelInfoSection.reportOrLeave.rawValue @@ -105,14 +108,16 @@ private enum ChannelInfoEntry: ItemListNodeEntry { return 8 case .notifications: return 9 - case .sharedMedia: + case .notificationSound: return 10 - case .report: + case .sharedMedia: return 11 - case .leave: + case .report: return 12 - case .deleteChannel: + case .leave: return 13 + case .deleteChannel: + return 14 } } @@ -228,6 +233,12 @@ private enum ChannelInfoEntry: ItemListNodeEntry { } else { return false } + case let .notificationSound(lhsTheme, lhsText, lhsValue): + if case let .notificationSound(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -283,6 +294,10 @@ private enum ChannelInfoEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.changeNotificationMuteSettings() }) + case let .notificationSound(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + arguments.changeNotificationSoundSettings() + }) case let .report(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.reportChannel() @@ -361,13 +376,12 @@ private struct ChannelInfoEditingState: Equatable { } } -private func channelInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: ChannelInfoState) -> [ChannelInfoEntry] { +private func channelInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, globalNotificationSettings: GlobalNotificationSettings, state: ChannelInfoState) -> [ChannelInfoEntry] { var entries: [ChannelInfoEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { let canEditChannel = peer.hasAdminRights(.canChangeInfo) let canEditMembers = peer.hasAdminRights(.canBanUsers) - let isPublic = peer.username != nil let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditChannel ? state.editingState?.editingName : nil, updatingName: nil) entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) @@ -424,7 +438,16 @@ private func channelInfoEntries(account: Account, presentationData: Presentation } entries.append(ChannelInfoEntry.notifications(theme: presentationData.theme, text: presentationData.strings.GroupInfo_Notifications, value: notificationsText)) } - entries.append(ChannelInfoEntry.sharedMedia(theme: presentationData.theme, text: presentationData.strings.GroupInfo_SharedMedia)) + if state.editingState != nil { + var messageSound: PeerMessageSound = .default + if let settings = view.notificationSettings as? TelegramPeerNotificationSettings { + messageSound = settings.messageSound + } + + entries.append(ChannelInfoEntry.notificationSound(theme: presentationData.theme, text: presentationData.strings.GroupInfo_Sound, value: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: messageSound, default: globalNotificationSettings.effective.groupChats.sound))) + } else { + entries.append(ChannelInfoEntry.sharedMedia(theme: presentationData.theme, text: presentationData.strings.GroupInfo_SharedMedia)) + } if peer.flags.contains(.isCreator) { if state.editingState != nil { @@ -623,6 +646,17 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, changeNotificationSoundSettings: { + let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } |> deliverOnMainQueue).start(next: { settings in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.privateChats.sound, completion: { sound in + let _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start() + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) }, openSharedMedia: { if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) @@ -658,10 +692,18 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr displayAddressNameContextMenuImpl?(text) }) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId)) - |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, ChannelInfoEntry.ItemGenerationArguments)) in + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [globalNotificationsKey])) + |> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, ChannelInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) + var globalNotificationSettings: GlobalNotificationSettings = GlobalNotificationSettings.defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + var canManageChannel = false if let peer = peer as? TelegramChannel { if peer.flags.contains(.isCreator) { @@ -733,7 +775,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr })) }) } - } else if canManageChannel { + } else { rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let channel = peer as? TelegramChannel, case .broadcast = channel.info { var text = "" @@ -748,7 +790,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } 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) + let listState = ItemListNodeState(entries: channelInfoEntries(account: account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .plain) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 24c6454074..a9e945ef7d 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -47,7 +47,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { let peer = self.peer - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in peerSelected(peer) }) } @@ -173,6 +173,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } }) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } } deinit { diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 0b5364328c..4a0dec2f21 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -52,7 +52,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { })) for participant in participants { - items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, selection: .none, index: nil, header: nil, action: { peer in + items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { peer in if let strongSelf = self { strongSelf.requestOpenPeerFromSearch?(peer) } diff --git a/TelegramUI/ChatAvatarNavigationNode.swift b/TelegramUI/ChatAvatarNavigationNode.swift index 1cb6e8f686..2127e7d990 100644 --- a/TelegramUI/ChatAvatarNavigationNode.swift +++ b/TelegramUI/ChatAvatarNavigationNode.swift @@ -32,12 +32,12 @@ final class ChatAvatarNavigationNode: ASDisplayNode { if !self.avatarNode.bounds.size.equalTo(bounds.size) { self.avatarNode.font = smallFont } - self.avatarNode.frame = bounds + self.avatarNode.frame = bounds.offsetBy(dx: 8.0, dy: 0.0) } else { if !self.avatarNode.bounds.size.equalTo(bounds.size) { self.avatarNode.font = normalFont } - self.avatarNode.frame = bounds.offsetBy(dx: 2.0, dy: 1.0) + self.avatarNode.frame = bounds.offsetBy(dx: 10.0, dy: 1.0) } } } diff --git a/TelegramUI/ChatBubbleVideoDecoration.swift b/TelegramUI/ChatBubbleVideoDecoration.swift new file mode 100644 index 0000000000..2daa7bab48 --- /dev/null +++ b/TelegramUI/ChatBubbleVideoDecoration.swift @@ -0,0 +1,66 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit + +final class ChatBubbleVideoDecoration: UniversalVideoDecoration { + let backgroundNode: ASDisplayNode? = nil + let contentContainerNode: ASDisplayNode + let foregroundNode: ASDisplayNode? = nil + + private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? + + private var validLayoutSize: CGSize? + + init(cornerRadius: CGFloat) { + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.clipsToBounds = true + self.contentContainerNode.cornerRadius = cornerRadius + } + + func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + if self.contentNode !== contentNode { + let previous = self.contentNode + self.contentNode = contentNode + + if let previous = previous { + if previous.supernode === self.contentContainerNode { + previous.removeFromSupernode() + } + } + + if let contentNode = contentNode { + if contentNode.supernode !== self.contentContainerNode { + self.contentContainerNode.addSubnode(contentNode) + if let validLayoutSize = self.validLayoutSize { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + } + } + } + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayoutSize = size + + if let backgroundNode = self.backgroundNode { + transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } + if let foregroundNode = self.foregroundNode { + transition.updateFrame(node: foregroundNode, frame: CGRect(origin: CGPoint(), size: size)) + } + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + contentNode.updateLayout(size: size, transition: transition) + } + } + + func setStatus(_ status: Signal) { + } + + func tap() { + } +} + diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 7ce8d514bd..b281bdbe6b 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -7,10 +7,15 @@ import AsyncDisplayKit import TelegramCore import SafariServices +public enum ChatControllerPeekActions { + case standard + case remove(() -> Void) +} + public class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() - public let v = 1 + public var peekActions: ChatControllerPeekActions = .standard private let account: Account public let peerId: PeerId @@ -108,7 +113,7 @@ public class ChatController: TelegramController { 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) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, accountPeerId: account.peerId) super.init(account: account) @@ -228,6 +233,7 @@ public class ChatController: TelegramController { if let _ = galleryMedia { let gallery = SecretMediaPreviewController(account: strongSelf.account, messageId: messageId) strongSelf.secretMediaPreviewController = gallery + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(gallery, in: .window(.root)) } } @@ -298,7 +304,7 @@ public class ChatController: TelegramController { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() + strongSelf.sendMessages([.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) } }, sendSticker: { [weak self] file in if let strongSelf = self { @@ -309,7 +315,7 @@ public class ChatController: TelegramController { }) } }) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) } }, sendGif: { [weak self] file in if let strongSelf = self { @@ -320,7 +326,7 @@ public class ChatController: TelegramController { }) } }) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { @@ -377,6 +383,7 @@ public class ChatController: TelegramController { strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) case let .url(url): if isGame { + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(GameController(account: strongSelf.account, url: url, message: message), in: .window(.root)) } else { strongSelf.openUrl(url) @@ -417,7 +424,7 @@ public class ChatController: TelegramController { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]).start() + strongSelf.sendMessages([.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]) } }, openInstantPage: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -425,7 +432,45 @@ public class ChatController: TelegramController { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let _ = content.instantPage { - let pageController = InstantPageController(account: strongSelf.account, webPage: webpage) + var textUrl: String? + if let pageUrl = URL(string: content.url) { + inner: for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + for entity in attribute.entities { + switch entity.type { + case let .TextUrl(url): + if let parsedUrl = URL(string: url) { + if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { + textUrl = url + } + } + case .Url: + let nsText = message.text as NSString + var entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if entityRange.location + entityRange.length > nsText.length { + entityRange.location = max(0, nsText.length - entityRange.length) + entityRange.length = nsText.length - entityRange.location + } + let url = nsText.substring(with: entityRange) + if let parsedUrl = URL(string: url) { + if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path { + textUrl = url + } + } + default: + break + } + } + break inner + } + } + } + var anchor: String? + if let textUrl = textUrl, let anchorRange = textUrl.range(of: "#") { + anchor = String(textUrl[anchorRange.upperBound...]) + } + + let pageController = InstantPageController(account: strongSelf.account, webPage: webpage, anchor: anchor) (strongSelf.navigationController as? NavigationController)?.pushViewController(pageController) } break @@ -447,33 +492,10 @@ public class ChatController: TelegramController { }) } }, openMessageShareMenu: { [weak self] id in - if let strongSelf = self { - var copyLink: (() -> Void)? - var shareAction: (([PeerId]) -> Void)? - let shareController = ShareController(account: strongSelf.account, shareAction: { peerIds in - shareAction?(peerIds) - }, defaultAction: ShareControllerAction(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, action: { - copyLink?() - })) + if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { + let shareController = ShareController(account: strongSelf.account, subject: .message(message)) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(shareController, in: .window(.root)) - shareAction = { [weak shareController] peerIds in - shareController?.dismiss() - - if let strongSelf = self { - for peerId in peerIds { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.forward(source: id)]).start() - } - } - } - copyLink = { [weak shareController] in - shareController?.dismiss() - - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { - if let addressName = peer.addressName { - UIPasteboard.general.string = "https://t.me/\(addressName)/\(id.id)" - } - } - } } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) @@ -525,6 +547,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .peerMention(peerId, mention): let actionSheet = ActionSheetController() @@ -549,6 +572,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .mention(mention): let actionSheet = ActionSheetController() @@ -569,6 +593,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .command(command): let actionSheet = ActionSheetController() @@ -577,7 +602,7 @@ public class ChatController: TelegramController { ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: [], media: nil, replyToMessageId: nil)]).start() + strongSelf.sendMessages([.message(text: command, attributes: [], media: nil, replyToMessageId: nil)]) } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in @@ -589,6 +614,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(hashtag): let actionSheet = ActionSheetController() @@ -610,6 +636,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) } } @@ -619,6 +646,7 @@ public class ChatController: TelegramController { for media in message.media { if let invoice = media as? TelegramMediaInvoice { if let receiptMessageId = invoice.receiptMessageId { + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(BotReceiptController(account: strongSelf.account, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { strongSelf.present(BotCheckoutController(account: strongSelf.account, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -627,6 +655,7 @@ public class ChatController: TelegramController { } } } + }, openSearch: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction @@ -911,7 +940,13 @@ public class ChatController: TelegramController { } else if let cachedData = combinedInitialData.cachedData as? CachedSecretChatData { canReport = cachedData.reportStatus == .canReport } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage).updatedPinnedMessageId(pinnedMessageId).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in + var pinnedMessage: Message? + if let pinnedMessageId = pinnedMessageId { + if let cachedDataMessages = combinedInitialData.cachedDataMessages { + pinnedMessage = cachedDataMessages[pinnedMessageId] + } + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage).updatedPinnedMessageId(pinnedMessageId).updatedPinnedMessage(pinnedMessage).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in if pinnedMessageId != nil { if !context.contains(where: { switch $0 { @@ -945,13 +980,16 @@ public class ChatController: TelegramController { } }) }) - if let readStateData = combinedInitialData.readStateData { - let globalRemainingUnreadCount = readStateData.totalUnreadCount - readStateData.unreadCount - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" - } else { - strongSelf.navigationItem.badge = "" - } + } + if let readStateData = combinedInitialData.readStateData, let notificationSettings = readStateData.notificationSettings { + var globalRemainingUnreadCount = readStateData.totalUnreadCount + if !notificationSettings.isRemovedFromTotalUnreadCount { + globalRemainingUnreadCount -= readStateData.unreadCount + } + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" } } } @@ -973,7 +1011,7 @@ public class ChatController: TelegramController { } }) - self.cachedDataDisposable = self.chatDisplayNode.historyNode.cachedPeerData.start(next: { [weak self] cachedData in + self.cachedDataDisposable = self.chatDisplayNode.historyNode.cachedPeerDataAndMessages.start(next: { [weak self] cachedData, messages in if let strongSelf = self { var pinnedMessageId: MessageId? var peerIsBlocked: Bool = false @@ -989,9 +1027,24 @@ public class ChatController: TelegramController { } else if let cachedData = cachedData as? CachedSecretChatData { canReport = cachedData.reportStatus == .canReport } - if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || strongSelf.presentationInterfaceState.canReportPeer != canReport { + + var pinnedMessage: Message? + if let pinnedMessageId = pinnedMessageId { + pinnedMessage = messages?[pinnedMessageId] + } + + var pinnedMessageUpdated = false + if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage { + if current.id != updated.id || current.stableVersion != updated.stableVersion { + pinnedMessageUpdated = true + } + } else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) { + pinnedMessageUpdated = true + } + + if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || strongSelf.presentationInterfaceState.canReportPeer != canReport || pinnedMessageUpdated { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedPinnedMessageId(pinnedMessageId).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in + return state.updatedPinnedMessageId(pinnedMessageId).updatedPinnedMessage(pinnedMessage).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in if pinnedMessageId != nil { if !context.contains(where: { switch $0 { @@ -1126,7 +1179,7 @@ public class ChatController: TelegramController { stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { @@ -1169,6 +1222,7 @@ public class ChatController: TelegramController { }, openContacts: { if let strongSelf = self { let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.DialogList_SelectContact }) + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) strongSelf.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let peerId = peerId { @@ -1186,7 +1240,7 @@ public class ChatController: TelegramController { } }) let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [message]).start() + strongSelf.sendMessages([message]) } })) } @@ -1376,6 +1430,7 @@ public class ChatController: TelegramController { } } } + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window(.root)) } } @@ -1573,7 +1628,7 @@ public class ChatController: TelegramController { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]).start() + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]) } } }, sendBotStart: { [weak self] payload in @@ -1633,7 +1688,7 @@ public class ChatController: TelegramController { }) } }) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() + strongSelf.sendMessages([.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]) } }, unblockPeer: { [weak self] in self?.unblockPeer() @@ -1660,7 +1715,7 @@ public class ChatController: TelegramController { pinAction(true) })]), in: .window(.root)) } else { - if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.id { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) }) @@ -1687,9 +1742,9 @@ public class ChatController: TelegramController { } })]), in: .window(.root)) } else { - if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessage.id) }) }) }) } } @@ -1734,24 +1789,35 @@ public class ChatController: TelegramController { self?.present(controller, in: .window(.root)) }, 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), .total]) |> deliverOnMainQueue).start(next: { [weak self] items in + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(self.peerId), .total]) + let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: self.peerId) + self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 - if let count = items.count(for: .peer(strongSelf.peerId)) { - unreadCount = count - } var totalCount: Int32 = 0 - if let count = items.count(for: .total) { - totalCount = count + + if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { + if let count = view.count(for: .peer(strongSelf.peerId)) { + unreadCount = count + } + if let count = view.count(for: .total) { + totalCount = count + } } strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount - let globalRemainingUnreadCount = totalCount - unreadCount - if globalRemainingUnreadCount > 0 { - strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" - } else { - strongSelf.navigationItem.badge = "" + if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings { + var globalRemainingUnreadCount = totalCount + if !notificationSettings.isRemovedFromTotalUnreadCount { + globalRemainingUnreadCount -= unreadCount + } + + if globalRemainingUnreadCount > 0 { + strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)" + } else { + strongSelf.navigationItem.badge = "" + } } } }) @@ -1970,19 +2036,23 @@ public class ChatController: TelegramController { self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive) } - if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { - self.navigationItem.setLeftBarButton(button.buttonItem, animated: true) - self.leftNavigationButton = button + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { + if self.leftNavigationButton != button { + self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) + self.leftNavigationButton = button + } } else if let _ = self.leftNavigationButton { - self.navigationItem.setLeftBarButton(nil, animated: true) + self.navigationItem.setLeftBarButton(nil, animated: animated) self.leftNavigationButton = nil } - if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { - self.navigationItem.setRightBarButton(button.buttonItem, animated: true) - self.rightNavigationButton = button + if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { + if self.rightNavigationButton != button { + self.navigationItem.setRightBarButton(button.buttonItem, animated: animated) + self.rightNavigationButton = button + } } else if let _ = self.rightNavigationButton { - self.navigationItem.setRightBarButton(nil, animated: true) + self.navigationItem.setRightBarButton(nil, animated: animated) self.rightNavigationButton = nil } @@ -2042,6 +2112,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) case .openChatInfo: self.navigationActionDisposable.set((self.peerView.get() @@ -2064,6 +2135,7 @@ public class ChatController: TelegramController { let legacyController = LegacyController(presentation: .modal(animateIn: true)) let controller = generator(legacyController.context) legacyController.bind(controller: controller) + legacyController.deferScreenEdgeGestures = [.top] configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer) controller.descriptionGenerator = legacyAssetPickerItemGenerator() @@ -2078,6 +2150,7 @@ public class ChatController: TelegramController { legacyController.dismiss() } } + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(legacyController, in: .window(.root)) } }) @@ -2085,6 +2158,7 @@ public class ChatController: TelegramController { } private func presentMapPicker() { + self.chatDisplayNode.dismissInput() self.present(legacyLocationPickerController(sendLocation: { [weak self] coordinate, venue in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId @@ -2096,11 +2170,17 @@ public class ChatController: TelegramController { } }) let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue), replyToMessageId: replyMessageId) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [message]).start() + strongSelf.sendMessages([message]) } }), in: .window(.root)) } + private func sendMessages(_ messages: [EnqueueMessage]) { + let _ = enqueueMessages(account: self.account, peerId: self.peerId, messages: messages).start(next: { [weak self] _ in + self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }) + } + private func enqueueMediaMessages(signals: [Any]?) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: self.peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { @@ -2112,7 +2192,7 @@ public class ChatController: TelegramController { }) } }) - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }).start() + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) } @@ -2127,7 +2207,7 @@ public class ChatController: TelegramController { }) } }) - let _ = enqueueMessages(account: self.account, peerId: self.peerId, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]).start() + self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) } } @@ -2173,7 +2253,7 @@ public class ChatController: TelegramController { waveformBuffer = MemoryBuffer(data: waveform) } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]).start() + strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]) strongSelf.audioRecorderFeedback?.success() strongSelf.audioRecorderFeedback = nil @@ -2384,6 +2464,7 @@ public class ChatController: TelegramController { } } } + self.chatDisplayNode.dismissInput() self.present(controller, in: .window(.root)) } case let .withBotStartPayload(_): @@ -2443,6 +2524,7 @@ public class ChatController: TelegramController { actionSheet?.dismissAnimated() }) ])]) + self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } } @@ -2495,6 +2577,8 @@ public class ChatController: TelegramController { break case let .channelMessage(peerId, messageId): (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId)) + case let .stickerPack(name): + strongSelf.present(StickerPackPreviewController(account: strongSelf.account, stickerPack: .name(name)), in: .window(.root)) } } })) @@ -2518,32 +2602,50 @@ public class ChatController: TelegramController { return data.with { [weak self] data -> [UIPreviewActionItem] in var items: [UIPreviewActionItem] = [] if let data = data, let strongSelf = self { - if let _ = data.peer as? TelegramUser { - items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in - if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil)]).start() - } - })) - } + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } - if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { - if case .unmuted = notificationSettings.muteState { - let muteItem = UIPreviewAction(title: strongSelf.presentationData.strings.Conversation_Mute, style: .default, handler: { _, _ in - if let strongSelf = self { - let muteState: PeerMuteState = .muted(until: Int32.max) - let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() + switch strongSelf.peekActions { + case .standard: + if let _ = data.peer as? TelegramUser { + items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil)]).start() + } + })) + } + + if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + items.append(UIPreviewAction(title: presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() + } + })) + } else { + let muteInterval: Int32 + if let _ = data.peer as? TelegramChannel { + muteInterval = Int32.max + } else { + muteInterval = 1 * 60 * 60 + } + let title: String + if muteInterval == Int32.max { + title = presentationData.strings.Conversation_Mute + } else { + title = muteForIntervalString(strings: presentationData.strings, value: muteInterval) + } + + items.append(UIPreviewAction(title: title, style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = mutePeer(account: strongSelf.account, peerId: strongSelf.peerId, for: muteInterval).start() + } + })) } - }) - items.append(muteItem) - } else { - let unmuteItem = UIPreviewAction(title: strongSelf.presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in - if let strongSelf = self { - let muteState: PeerMuteState = .unmuted - let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() - } - }) - items.append(unmuteItem) - } + } + case let .remove(action): + items.append(UIPreviewAction(title: presentationData.strings.Common_Delete, style: .destructive, handler: { _, _ in + action() + })) } } return items diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index e8a473e2ab..0e5039f13d 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -62,13 +62,14 @@ public final class ChatControllerInteraction { let callPeer: (PeerId) -> Void let longTap: (ChatControllerInteractionLongTapAction) -> Void let openCheckoutOrReceipt: (MessageId) -> Void + let openSearch: () -> 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) { + 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, openSearch: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -94,6 +95,7 @@ public final class ChatControllerInteraction { self.callPeer = callPeer self.longTap = longTap self.openCheckoutOrReceipt = openCheckoutOrReceipt + self.openSearch = openSearch self.automaticMediaDownloadSettings = automaticMediaDownloadSettings } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 473058fa35..9ae2aa1439 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -21,6 +21,7 @@ class ChatControllerNode: ASDisplayNode { let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode + let loadingNode: ChatLoadingNode private var searchNavigationNode: ChatSearchNavigationContentNode? @@ -60,6 +61,18 @@ class ChatControllerNode: ASDisplayNode { private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + private var isLoading: Bool = false { + didSet { + if self.isLoading != oldValue { + if self.isLoading { + self.historyNode.supernode?.insertSubnode(self.loadingNode, aboveSubnode: self.historyNode) + } else { + self.loadingNode.removeFromSupernode() + } + } + } + } + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar) { self.account = account self.peerId = peerId @@ -78,6 +91,7 @@ class ChatControllerNode: ASDisplayNode { self.titleAccessoryPanelContainer.clipsToBounds = true self.historyNode = ChatHistoryListNode(account: account, peerId: peerId, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction) + self.loadingNode = ChatLoadingNode(theme: chatPresentationInterfaceState.theme) self.inputPanelBackgroundNode = ASDisplayNode() self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor @@ -99,6 +113,16 @@ class ChatControllerNode: ASDisplayNode { assert(Queue.mainQueue().isCurrent()) + self.historyNode.setLoadStateUpdated { [weak self] loadState in + if let strongSelf = self { + if case .loading = loadState { + strongSelf.isLoading = true + } else { + strongSelf.isLoading = false + } + } + } + var backgroundImage: UIImage? let wallpaper = chatPresentationInterfaceState.chatWallpaper if wallpaper == backgroundImageForWallpaper?.0 { @@ -198,7 +222,11 @@ class ChatControllerNode: ASDisplayNode { } } - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start(next: { _ in + if let strongSelf = self { + strongSelf.historyNode.scrollToEndOfHistory() + } + }) } } } @@ -264,10 +292,36 @@ class ChatControllerNode: ASDisplayNode { self.titleAccessoryPanelNode = nil } + var dismissedInputPanelNode: ASDisplayNode? + var dismissedAccessoryPanelNode: ASDisplayNode? + var dismissedInputContextPanelNode: ChatInputContextPanelNode? + + var inputPanelSize: CGSize? + var immediatelyLayoutInputPanelAndAnimateAppearance = false + if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { + if inputPanelNode !== self.inputPanelNode { + if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + inputTextPanelNode.ensureUnfocused() + } + dismissedInputPanelNode = self.inputPanelNode + immediatelyLayoutInputPanelAndAnimateAppearance = true + 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, 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) + } + } else { + dismissedInputPanelNode = self.inputPanelNode + self.inputPanelNode = nil + } + var dismissedInputNode: ChatInputNode? var immediatelyLayoutInputNodeAndAnimateAppearance = false var inputNodeHeight: CGFloat? - if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction) { + if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode) { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() } @@ -328,6 +382,9 @@ class ChatControllerNode: ASDisplayNode { self.historyNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.historyNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + self.loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) + transition.updateFrame(node: self.loadingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + let listViewCurve: ListViewAnimationCurve if curve == 7 { listViewCurve = .Spring(duration: duration) @@ -335,32 +392,6 @@ class ChatControllerNode: ASDisplayNode { listViewCurve = .Default } - var dismissedInputPanelNode: ASDisplayNode? - var dismissedAccessoryPanelNode: ASDisplayNode? - var dismissedInputContextPanelNode: ChatInputContextPanelNode? - - var inputPanelSize: CGSize? - var immediatelyLayoutInputPanelAndAnimateAppearance = false - if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { - if inputPanelNode !== self.inputPanelNode { - if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { - inputTextPanelNode.ensureUnfocused() - } - dismissedInputPanelNode = self.inputPanelNode - immediatelyLayoutInputPanelAndAnimateAppearance = true - 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, 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) - } - } else { - dismissedInputPanelNode = self.inputPanelNode - self.inputPanelNode = nil - } - var accessoryPanelSize: CGSize? var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift index c7e16f9858..e11e8a7ba0 100644 --- a/TelegramUI/ChatEmptyItem.swift +++ b/TelegramUI/ChatEmptyItem.swift @@ -10,15 +10,17 @@ private let messageFont = Font.medium(14.0) final class ChatEmptyItem: ListViewItem { fileprivate let theme: PresentationTheme fileprivate let strings: PresentationStrings + fileprivate let tagMask: MessageTags? - init(theme: PresentationTheme, strings: PresentationStrings) { + init(theme: PresentationTheme, strings: PresentationStrings, tagMask: MessageTags?) { self.theme = theme self.strings = strings + self.tagMask = tagMask } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { - let node = ChatEmptyItemNode() + let node = ChatEmptyItemNode(rotated: self.tagMask == nil) let nodeLayout = node.asyncLayout() let (layout, apply) = nodeLayout(self, width) @@ -58,6 +60,8 @@ final class ChatEmptyItem: ListViewItem { } final class ChatEmptyItemNode: ListViewItemNode { + private let rotated: Bool + var controllerInteraction: ChatControllerInteraction? let offsetContainer: ASDisplayNode @@ -67,7 +71,8 @@ final class ChatEmptyItemNode: ListViewItemNode { private var theme: PresentationTheme? - init() { + init(rotated: Bool) { + self.rotated = rotated self.offsetContainer = ASDisplayNode() self.backgroundNode = ASImageNode() @@ -76,9 +81,11 @@ final class ChatEmptyItemNode: ListViewItemNode { self.iconNode = ASImageNode() self.textNode = TextNode() - super.init(layerBacked: false, dynamicBounce: true, rotated: true) + super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + if rotated { + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + } self.addSubnode(self.offsetContainer) self.offsetContainer.addSubnode(self.backgroundNode) @@ -92,16 +99,27 @@ final class ChatEmptyItemNode: ListViewItemNode { let currentTheme = self.theme return { [weak self] item, width in var updatedBackgroundImage: UIImage? - var updatedIconImage: UIImage? - let iconImage = PresentationResourcesChat.chatEmptyItemIconImage(item.theme) + let iconImage: UIImage? = PresentationResourcesChat.chatEmptyItemIconImage(item.theme) if currentTheme !== item.theme { updatedBackgroundImage = PresentationResourcesChat.chatEmptyItemBackgroundImage(item.theme) - updatedIconImage = iconImage } - let attributedText = NSAttributedString(string: item.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor, paragraphAlignment: .center) + let attributedText: NSAttributedString + if let tagMask = item.tagMask { + let text: String + if tagMask == .photoOrVideo { + text = item.strings.SharedMedia_EmptyText + } else if tagMask == .file { + text = item.strings.SharedMedia_EmptyFilesText + } else { + text = "" + } + attributedText = NSAttributedString(string: text, font: messageFont, textColor: item.theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + } else { + attributedText = NSAttributedString(string: item.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor, paragraphAlignment: .center) + } let horizontalEdgeInset: CGFloat = 10.0 let horizontalContentInset: CGFloat = 12.0 @@ -131,9 +149,7 @@ final class ChatEmptyItemNode: ListViewItemNode { strongSelf.backgroundNode.image = updatedBackgroundImage } - if let updatedIconImage = updatedIconImage { - strongSelf.iconNode.image = updatedIconImage - } + strongSelf.iconNode.image = iconImage let _ = textApply() strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) @@ -149,7 +165,7 @@ final class ChatEmptyItemNode: ListViewItemNode { if height.isLessThanOrEqualTo(0.0) { transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) } else { - transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: floor(height) / 2.0), size: self.offsetContainer.bounds.size)) + transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.rotated ? (floor(height) / 2.0) : (-floor(height) / 4.0)), size: self.offsetContainer.bounds.size)) } } diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index e174acdba7..8c27e75b09 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, theme: PresentationTheme, strings: PresentationStrings) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, theme: PresentationTheme, strings: PresentationStrings) -> [ChatHistoryEntry] { var entries: [ChatHistoryEntry] = [] for entry in view.entries { @@ -52,7 +52,24 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { entries.insert(.ChatInfoEntry(botInfo.description, theme, strings), at: 0) } else if view.entries.isEmpty && includeEmptyEntry { - entries.insert(.EmptyChatInfoEntry(theme, strings), at: 0) + entries.insert(.EmptyChatInfoEntry(theme, strings, view.tagMask), at: 0) + } + } + } else if includeSearchEntry { + if view.laterId == nil { + var hasMessages = false + loop: for entry in view.entries { + if case .MessageEntry = entry { + hasMessages = true + break loop + } + } + if hasMessages { + entries.append(.SearchEntry(theme, strings)) + } else if view.entries.isEmpty { + if view.tagMask != nil { + entries.insert(.EmptyChatInfoEntry(theme, strings, view.tagMask), at: 0) + } } } } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index 558725f642..77ff56f5bd 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -6,7 +6,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { case MessageEntry(Message, PresentationTheme, PresentationStrings, Bool, MessageHistoryEntryMonthLocation?) case UnreadEntry(MessageIndex, PresentationTheme, PresentationStrings) case ChatInfoEntry(String, PresentationTheme, PresentationStrings) - case EmptyChatInfoEntry(PresentationTheme, PresentationStrings) + case EmptyChatInfoEntry(PresentationTheme, PresentationStrings, MessageTags?) + case SearchEntry(PresentationTheme, PresentationStrings) var stableId: UInt64 { switch self { @@ -20,6 +21,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return UInt64(4) << 40 case .EmptyChatInfoEntry: return UInt64(5) << 40 + case .SearchEntry: + return UInt64(6) << 40 } } @@ -35,81 +38,89 @@ enum ChatHistoryEntry: Identifiable, Comparable { return MessageIndex.absoluteLowerBound() case .EmptyChatInfoEntry: return MessageIndex.absoluteLowerBound() + case .SearchEntry: + return MessageIndex.absoluteLowerBound() } } -} -func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { - switch lhs { - case let .HoleEntry(lhsHole, lhsTheme, lhsStrings): - if case let .HoleEntry(rhsHole, rhsTheme, rhsStrings) = rhs, lhsHole == rhsHole, lhsTheme === rhsTheme, lhsStrings === rhsStrings { - return true - } else { - return false - } - case let .MessageEntry(lhsMessage, lhsTheme, lhsStrings, lhsRead, _): - switch rhs { - case let .MessageEntry(rhsMessage, rhsTheme, rhsStrings, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - if lhsMessage.stableVersion != rhsMessage.stableVersion { - return false - } - if lhsMessage.media.count != rhsMessage.media.count { - return false - } - for i in 0 ..< lhsMessage.media.count { - if !lhsMessage.media[i].isEqual(rhsMessage.media[i]) { + static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + switch lhs { + case let .HoleEntry(lhsHole, lhsTheme, lhsStrings): + if case let .HoleEntry(rhsHole, rhsTheme, rhsStrings) = rhs, lhsHole == rhsHole, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .MessageEntry(lhsMessage, lhsTheme, lhsStrings, lhsRead, _): + switch rhs { + case let .MessageEntry(rhsMessage, rhsTheme, rhsStrings, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + if lhsTheme !== rhsTheme { return false } - } - if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { - return false - } - if !lhsMessage.associatedMessages.isEmpty { - for (id, message) in lhsMessage.associatedMessages { - if let otherMessage = rhsMessage.associatedMessages[id] { - if otherMessage.stableVersion != message.stableVersion { - return false + if lhsStrings !== rhsStrings { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsMessage.media.count != rhsMessage.media.count { + return false + } + for i in 0 ..< lhsMessage.media.count { + if !lhsMessage.media[i].isEqual(rhsMessage.media[i]) { + return false + } + } + if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { + return false + } + if !lhsMessage.associatedMessages.isEmpty { + for (id, message) in lhsMessage.associatedMessages { + if let otherMessage = rhsMessage.associatedMessages[id] { + if otherMessage.stableVersion != message.stableVersion { + return false + } } } } - } + return true + default: + return false + } + case let .UnreadEntry(lhsIndex, lhsTheme, lhsStrings): + if case let .UnreadEntry(rhsIndex, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true - default: + } else { return false - } - case let .UnreadEntry(lhsIndex, lhsTheme, lhsStrings): - if case let .UnreadEntry(rhsIndex, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { - return true - } else { - return false - } - case let .ChatInfoEntry(lhsText, lhsTheme, lhsStrings): - if case let .ChatInfoEntry(rhsText, rhsTheme, rhsStrings) = rhs, lhsText == rhsText, lhsTheme === rhsTheme, lhsStrings === rhsStrings { - return true - } else { - return false - } - case let .EmptyChatInfoEntry(lhsTheme, lhsStrings): - if case let .EmptyChatInfoEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { - return true - } else { - return false - } - } -} - -func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { - let lhsIndex = lhs.index - let rhsIndex = rhs.index - if lhsIndex == rhsIndex { - return lhs.stableId < rhs.stableId - } else { - return lhsIndex < rhsIndex + } + case let .ChatInfoEntry(lhsText, lhsTheme, lhsStrings): + if case let .ChatInfoEntry(rhsText, rhsTheme, rhsStrings) = rhs, lhsText == rhsText, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .EmptyChatInfoEntry(lhsTheme, lhsStrings, lhsTagMask): + if case let .EmptyChatInfoEntry(rhsTheme, rhsStrings, rhsTagMask) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTagMask == rhsTagMask { + return true + } else { + return false + } + case let .SearchEntry(lhsTheme, lhsStrings): + if case let .SearchEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + } + } + + static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + let lhsIndex = lhs.index + let rhsIndex = rhs.index + if lhsIndex == rhsIndex { + return lhs.stableId < rhs.stableId + } else { + return lhsIndex < rhsIndex + } } } diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index a6d12630ab..ad1b3ca50c 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -25,7 +25,7 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) - case .ChatInfoEntry, .EmptyChatInfoEntry: + case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) } @@ -42,7 +42,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) - case .ChatInfoEntry, .EmptyChatInfoEntry: + case .ChatInfoEntry, .EmptyChatInfoEntry, .SearchEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) } @@ -174,6 +174,9 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { private var presentationData: PresentationData private let themeAndStringsPromise = Promise<(PresentationTheme, PresentationStrings)>() + public private(set) var loadState: ChatHistoryNodeLoadState? + private var loadStateUpdated: ((ChatHistoryNodeLoadState) -> Void)? + public init(account: Account, peerId: PeerId, messageId: MessageId?, tagMask: MessageTags?, controllerInteraction: ChatControllerInteraction) { self.account = account self.peerId = peerId @@ -205,6 +208,12 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { case .Loading: Queue.mainQueue().async { [weak self] in if let strongSelf = self { + let loadState: ChatHistoryNodeLoadState = .loading + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } + let historyState: ChatHistoryNodeHistoryState = .loading if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -233,10 +242,10 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -302,6 +311,10 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { self.historyDisposable.dispose() } + public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState) -> Void) { + self.loadStateUpdated = f + } + public func scrollToStartOfHistory() { self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true)) } @@ -337,6 +350,17 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { + let loadState: ChatHistoryNodeLoadState + if transition.historyView.filteredEntries.isEmpty { + loadState = .empty + } else { + loadState = .messages + } + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } + let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -363,6 +387,22 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.upperBound].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lowerBound].index) } + let loadState: ChatHistoryNodeLoadState + if let historyView = strongSelf.historyView { + if historyView.filteredEntries.isEmpty { + loadState = .empty + } else { + loadState = .messages + } + } else { + loadState = .loading + } + + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } + let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 848c43a201..0f49219005 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -24,17 +24,19 @@ enum ChatHistoryViewUpdateType { public struct ChatHistoryCombinedInitialReadStateData { public let unreadCount: Int32 public let totalUnreadCount: Int32 + public let notificationSettings: PeerNotificationSettings? } public struct ChatHistoryCombinedInitialData { let initialData: InitialMessageHistoryData? let buttonKeyboardMessage: Message? let cachedData: CachedPeerData? + let cachedDataMessages: [MessageId: Message]? let readStateData: ChatHistoryCombinedInitialReadStateData? } enum ChatHistoryViewUpdate { - case Loading(initialData: InitialMessageHistoryData?) + case Loading(initialData: ChatHistoryCombinedInitialData?) case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) } @@ -75,6 +77,7 @@ struct ChatHistoryViewTransition { let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? + let cachedDataMessages: [MessageId: Message]? let readStateData: ChatHistoryCombinedInitialReadStateData? let scrolledToIndex: MessageIndex? } @@ -90,6 +93,7 @@ struct ChatHistoryListViewTransition { let initialData: InitialMessageHistoryData? let keyboardButtonsMessage: Message? let cachedData: CachedPeerData? + let cachedDataMessages: [MessageId: Message]? let readStateData: ChatHistoryCombinedInitialReadStateData? let scrolledToIndex: MessageIndex? } @@ -134,8 +138,12 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, theme: theme, strings: strings), directionHint: entry.directionHint) case let .ChatInfoEntry(text, theme, strings): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, theme: theme, strings: strings), directionHint: entry.directionHint) - case let .EmptyChatInfoEntry(theme, strings): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings), directionHint: entry.directionHint) + case let .EmptyChatInfoEntry(theme, strings, tagMask): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings, tagMask: tagMask), directionHint: entry.directionHint) + case let .SearchEntry(theme, strings): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + controllerInteraction.openSearch() + }), directionHint: entry.directionHint) } } } @@ -165,14 +173,18 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, theme: theme, strings: strings), directionHint: entry.directionHint) case let .ChatInfoEntry(text, theme, strings): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, theme: theme, strings: strings), directionHint: entry.directionHint) - case let .EmptyChatInfoEntry(theme, strings): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings), directionHint: entry.directionHint) + case let .EmptyChatInfoEntry(theme, strings, tagMask): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings, tagMask: tagMask), directionHint: entry.directionHint) + case let .SearchEntry(theme, strings): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + controllerInteraction.openSearch() + }), directionHint: entry.directionHint) } } } private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex) + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex) } private final class ChatHistoryTransactionOpaqueState { @@ -211,9 +223,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return self._initialData.get() } - private let _cachedPeerData = Promise() - public var cachedPeerData: Signal { - return self._cachedPeerData.get() + private let _cachedPeerDataAndMessages = Promise<(CachedPeerData?, [MessageId: Message]?)>() + public var cachedPeerDataAndMessages: Signal<(CachedPeerData?, [MessageId: Message]?), NoError> { + return self._cachedPeerDataAndMessages.get() } private var _buttonKeyboardMessage = Promise(nil) @@ -251,6 +263,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in } + public private(set) var loadState: ChatHistoryNodeLoadState? + private var loadStateUpdated: ((ChatHistoryNodeLoadState) -> Void)? + + private var loadedMessagesFromCachedDataDisposable: Disposable? + public init(account: Account, peerId: PeerId, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode = .bubbles) { self.account = account self.peerId = peerId @@ -291,7 +308,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var additionalData: [AdditionalMessageHistoryViewData] = [] additionalData.append(.cachedPeerData(peerId)) + additionalData.append(.cachedPeerDataMessages(peerId)) additionalData.append(.totalUnreadCount) + additionalData.append(.peerNotificationSettings(peerId)) let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged @@ -311,8 +330,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let historyViewTransition = combineLatest(historyViewUpdate, self.themeAndStrings.get()) |> mapToQueue { [weak self] update, themeAndStrings -> Signal in let initialData: ChatHistoryCombinedInitialData? switch update { - case let .Loading(data): - let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil, readStateData: nil) + case let .Loading(combinedInitialData): initialData = combinedInitialData Queue.mainQueue().async { [weak self] in if let strongSelf = self { @@ -321,7 +339,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf._initialData.set(.single(combinedInitialData)) } - strongSelf._cachedPeerData.set(.single(nil)) + strongSelf._cachedPeerDataAndMessages.set(.single((nil, nil))) + + let loadState: ChatHistoryNodeLoadState = .loading + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } let historyState: ChatHistoryNodeHistoryState = .loading if strongSelf.currentHistoryState != historyState { @@ -352,10 +376,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles, includeChatInfoEntry: true, theme: themeAndStrings.0, strings: themeAndStrings.1)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: mode == .list && tagMask == nil, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -510,6 +534,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + self.loadedMessagesFromCachedDataDisposable = (self._cachedPeerDataAndMessages.get() |> map { dataAndMessages -> MessageId? in + return dataAndMessages.0?.messageIds.first + } |> distinctUntilChanged(isEqual: { $0 == $1 }) + |> mapToSignal { messageId -> Signal in + if let messageId = messageId { + return getMessagesLoadIfNecessary([messageId], postbox: account.postbox, network: account.network) |> map { _ -> Void in return Void() } + } else { + return .complete() + } + }).start() } deinit { @@ -517,6 +552,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.readHistoryDisposable.dispose() self.interactiveReadActionDisposable?.dispose() self.canReadHistoryDisposable?.dispose() + self.loadedMessagesFromCachedDataDisposable?.dispose() + } + + public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState) -> Void) { + self.loadStateUpdated = f } public func scrollToStartOfHistory() { @@ -524,7 +564,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } public func scrollToEndOfHistory() { - self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) + switch self.visibleContentOffset() { + case .known(0.0): + break + default: + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true)) + } } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { @@ -574,7 +619,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) } strongSelf.enqueuedHistoryViewTransition = (transition, { @@ -589,7 +634,19 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { - strongSelf._cachedPeerData.set(.single(transition.cachedData)) + strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) + + let loadState: ChatHistoryNodeLoadState + if transition.historyView.filteredEntries.isEmpty { + loadState = .empty + } else { + loadState = .messages + } + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } + let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState @@ -612,6 +669,22 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let strongSelf = self { strongSelf.historyView = transition.historyView + let loadState: ChatHistoryNodeLoadState + if let historyView = strongSelf.historyView { + if historyView.filteredEntries.isEmpty { + loadState = .empty + } else { + loadState = .messages + } + } else { + loadState = .loading + } + + if strongSelf.loadState != loadState { + strongSelf.loadState = loadState + strongSelf.loadStateUpdated?(loadState) + } + if let range = visibleRange.loadedRange { strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) @@ -624,9 +697,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData))) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) } - strongSelf._cachedPeerData.set(.single(transition.cachedData)) + strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState diff --git a/TelegramUI/ChatHistoryNavigationButtons.swift b/TelegramUI/ChatHistoryNavigationButtons.swift index 2cd0b206b6..20b80a73e8 100644 --- a/TelegramUI/ChatHistoryNavigationButtons.swift +++ b/TelegramUI/ChatHistoryNavigationButtons.swift @@ -103,4 +103,13 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { return completeSize } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subnode in self.subnodes { + if let result = subnode.hitTest(point.offsetBy(dx: -subnode.frame.minX, dy: -subnode.frame.minY), with: event) { + return result + } + } + return nil + } } diff --git a/TelegramUI/ChatHistoryNode.swift b/TelegramUI/ChatHistoryNode.swift index 216e43943e..97d8173c78 100644 --- a/TelegramUI/ChatHistoryNode.swift +++ b/TelegramUI/ChatHistoryNode.swift @@ -25,10 +25,19 @@ public enum ChatHistoryNodeHistoryState: Equatable { } } +public enum ChatHistoryNodeLoadState { + case loading + case empty + case messages +} + public protocol ChatHistoryNode: class { var historyState: ValuePromise { get } var preloadPages: Bool { get set } + var loadState: ChatHistoryNodeLoadState? { get } + func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState) -> Void) + func messageInCurrentHistoryView(_ id: MessageId) -> Message? func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func forEachItemNode(_ f: (ASDisplayNode) -> Void) diff --git a/TelegramUI/ChatHistorySearchContainerNode.swift b/TelegramUI/ChatHistorySearchContainerNode.swift new file mode 100644 index 0000000000..a89051b2f4 --- /dev/null +++ b/TelegramUI/ChatHistorySearchContainerNode.swift @@ -0,0 +1,304 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private enum ChatHistorySearchEntryStableId: Hashable { + case messageId(MessageId) + + static func ==(lhs: ChatHistorySearchEntryStableId, rhs: ChatHistorySearchEntryStableId) -> Bool { + switch lhs { + case let .messageId(messageId): + if case .messageId(messageId) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .messageId(messageId): + return messageId.hashValue + } + } +} + + +private enum ChatHistorySearchEntry: Comparable, Identifiable { + case message(Message, PresentationTheme, PresentationStrings) + + var stableId: ChatHistorySearchEntryStableId { + switch self { + case let .message(message, _, _): + return .messageId(message.id) + } + } + + static func ==(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool { + switch lhs { + case let .message(lhsMessage, lhsTheme, lhsStrings): + if case let .message(rhsMessage, rhsTheme, rhsStrings) = rhs { + if lhsMessage.id != rhsMessage.id { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool { + switch lhs { + case let .message(lhsMessage, _, _): + if case let .message(rhsMessage, _, _) = rhs { + return MessageIndex(lhsMessage) < MessageIndex(rhsMessage) + } else { + return false + } + } + } + + func item(account: Account, peerId: PeerId, interaction: ChatControllerInteraction) -> ListViewItem { + switch self { + case let .message(message, theme, strings): + return ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: interaction, message: message) + } + } +} + +private struct ChatHistorySearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let displayingResults: Bool +} + +private func chatHistorySearchContainerPreparedTransition(from fromEntries: [ChatHistorySearchEntry], to toEntries: [ChatHistorySearchEntry], displayingResults: Bool, account: Account, peerId: PeerId, interaction: ChatControllerInteraction) -> ChatHistorySearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerId: peerId, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerId: peerId, interaction: interaction), directionHint: nil) } + + return ChatHistorySearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) +} + +final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { + private let account: Account + + private let dimNode: ASDisplayNode + private let listNode: ListView + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private var currentEntries: [ChatHistorySearchEntry]? + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + private var enqueuedTransitions: [(ChatHistorySearchContainerTransition, Bool)] = [] + + init(account: Account, peerId: PeerId, tagMask: MessageTags, interfaceInteraction: ChatControllerInteraction) { + self.account = account + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + self.listNode = ListView() + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + self.listNode.isHidden = true + + let themeAndStringsPromise = self.themeAndStringsPromise + + let searchItems = searchQuery.get() + |> mapToSignal { query -> Signal<[ChatHistorySearchEntry]?, NoError> in + if let query = query, !query.isEmpty { + let foundRemoteMessages: Signal<[Message], NoError> = searchMessages(account: account, peerId: peerId, query: query, tagMask: tagMask) + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + + return combineLatest(foundRemoteMessages, themeAndStringsPromise.get()) + |> map { messages, themeAndStrings -> [ChatHistorySearchEntry]? in + if messages.isEmpty { + return nil + } else { + return messages.map { message -> ChatHistorySearchEntry in + return .message(message, themeAndStrings.0, themeAndStrings.1) + } + } + } + } else { + return .single(nil) + } + } + + let previousEntriesValue = Atomic<[ChatHistorySearchEntry]?>(value: nil) + + self.searchDisposable.set((searchItems + |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + let previousEntries = previousEntriesValue.swap(entries) + + let firstTime = previousEntries == nil + let transition = chatHistorySearchContainerPreparedTransition(from: previousEntries ?? [], to: entries ?? [], displayingResults: entries != nil, account: account, peerId: peerId, interaction: interfaceInteraction) + strongSelf.currentEntries = entries + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let firstValidLayout = self.containerLayout == nil + self.containerLayout = (layout, navigationBarHeight) + + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset + 2.0, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + + if firstValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func enqueueTransition(_ transition: ChatHistorySearchContainerTransition, firstTime: Bool) { + enqueuedTransitions.append((transition, firstTime)) + + if self.containerLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + if firstTime { + } else { + } + + let displayingResults = transition.displayingResults + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + if displayingResults != !strongSelf.listNode.isHidden { + strongSelf.listNode.isHidden = !displayingResults + strongSelf.dimNode.isHidden = displayingResults + strongSelf.backgroundColor = displayingResults ? UIColor.white : nil + } + } + }) + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + func messageForGallery(_ id: MessageId) -> Message? { + if let currentEntries = self.currentEntries { + for entry in currentEntries { + switch entry { + case let .message(message, _, _): + if message.id == id { + return message + } + } + } + } + return nil + } + + func updateHiddenMedia() { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() + } else if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } else if let itemNode = itemNode as? GridMessageItemNode { + itemNode.updateHiddenMedia() + } + } + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> ASDisplayNode? { + var transitionNode: ASDisplayNode? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } else if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } else if let itemNode = itemNode as? GridMessageItemNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + return transitionNode + } +} + diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 9638320208..2189ff83a6 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -17,24 +17,40 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? var readStateData: ChatHistoryCombinedInitialReadStateData? + var notificationSettings: PeerNotificationSettings? + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + default: + break + } + } for data in view.additionalData { switch data { case let .cachedPeerData(peerIdValue, value): if peerIdValue == peerId { cachedData = value } + case let .cachedPeerDataMessages(peerIdValue, value): + if peerIdValue == peerId { + cachedDataMessages = value + } case let .totalUnreadCount(totalUnreadCount): if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) } default: break } } + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) + if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) } else { var scrollPosition: ChatHistoryViewScrollPosition? @@ -69,7 +85,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { fadeIn = true - return .Loading(initialData: initialData) + return .Loading(initialData: combinedInitialData) } } } @@ -81,7 +97,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun for entry in view.entries.reversed() { if case .HoleEntry = entry { fadeIn = true - return .Loading(initialData: initialData) + return .Loading(initialData: combinedInitialData) } else { messageCount += 1 } @@ -92,7 +108,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } case let .InitialSearch(searchLocation, count): @@ -109,24 +125,40 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? var readStateData: ChatHistoryCombinedInitialReadStateData? + var notificationSettings: PeerNotificationSettings? + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + default: + break + } + } for data in view.additionalData { switch data { case let .cachedPeerData(peerIdValue, value): if peerIdValue == peerId { cachedData = value } + case let .cachedPeerDataMessages(peerIdValue, value): + if peerIdValue == peerId { + cachedDataMessages = value + } case let .totalUnreadCount(totalUnreadCount): if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) } default: break } } + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) + if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) } else { let anchorIndex = view.anchorIndex @@ -138,34 +170,49 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } - let maxIndex = min(view.entries.count, targetIndex + count / 2) - if maxIndex >= targetIndex { - for i in targetIndex ..< maxIndex { + if !view.entries.isEmpty { + let minIndex = max(0, targetIndex - count / 2) + let maxIndex = min(view.entries.count, targetIndex + count / 2) + for i in minIndex ..< maxIndex { if case .HoleEntry = view.entries[i] { fadeIn = true - return .Loading(initialData: initialData) + return .Loading(initialData: combinedInitialData) } } } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } case let .Navigation(index, anchorIndex): var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? var readStateData: ChatHistoryCombinedInitialReadStateData? + var notificationSettings: PeerNotificationSettings? + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + default: + break + } + } for data in view.additionalData { switch data { case let .cachedPeerData(peerIdValue, value): if peerIdValue == peerId { cachedData = value } + case let .cachedPeerDataMessages(peerIdValue, value): + if peerIdValue == peerId { + cachedDataMessages = value + } case let .totalUnreadCount(totalUnreadCount): if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) } default: break @@ -179,7 +226,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up @@ -187,16 +234,30 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun var first = true return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? var readStateData: ChatHistoryCombinedInitialReadStateData? + var notificationSettings: PeerNotificationSettings? + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + default: + break + } + } for data in view.additionalData { switch data { case let .cachedPeerData(peerIdValue, value): if peerIdValue == peerId { cachedData = value } + case let .cachedPeerDataMessages(peerIdValue, value): + if peerIdValue == peerId { + cachedDataMessages = value + } case let .totalUnreadCount(totalUnreadCount): if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) + readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount, notificationSettings: notificationSettings) } default: break @@ -211,7 +272,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData)) } } } diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 5d54e5ab69..a3eddc7cc8 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -143,7 +143,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -173,7 +173,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index 60934a9471..3d5d54255b 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -55,8 +55,13 @@ private func peerButtons(_ peer: Peer, isMuted: Bool) -> [ChatInfoTitleButton] { muteAction = .mute } - if let _ = peer as? TelegramUser { - return [.search, muteAction, .call, .info] + if let peer = peer as? TelegramUser { + var buttons: [ChatInfoTitleButton] = [.search, muteAction] + if peer.botInfo == nil { + buttons.append(.call) + } + buttons.append(.info) + return buttons } else if let channel = peer as? TelegramChannel { if channel.flags.contains(.isCreator) { return [.search, muteAction, .info] diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 3009d3511e..93806e5e46 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -50,7 +50,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> if c == " " { if index != startIndex { contextAddressRange = startIndex ..< index - index = inputText.index(after: index) + index = inputText.index(after: index) } break } else { @@ -86,7 +86,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> return (inputText.startIndex ..< inputText.endIndex, [.emoji], nil) } - var possibleTypes = PossibleContextQueryTypes([.command, .mention]) + var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag]) var definedType = false while true { @@ -132,7 +132,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> ChatPresentationInputQuery? { let inputState = chatPresentationInterfaceState.interfaceState.effectiveInputState if let (possibleQueryRange, possibleTypes, additionalStringRange) = textInputStateContextQueryRangeAndType(inputState) { - let query = inputState.inputText.substring(with: possibleQueryRange) + let query = String(inputState.inputText[possibleQueryRange]) if possibleTypes == [.emoji] { return .emoji(query) } else if possibleTypes == [.hashtag] { @@ -142,7 +142,7 @@ func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfa } else if possibleTypes == [.command] { return .command(query) } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { - let additionalString = inputState.inputText.substring(with: additionalStringRange) + let additionalString = String(inputState.inputText[additionalStringRange]) return .contextRequest(addressName: query, query: additionalString) } return nil diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index 26e15e9e58..394fae58a4 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -2,7 +2,10 @@ import Foundation import AsyncDisplayKit import TelegramCore -func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction) -> ChatInputNode? { +func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?) -> ChatInputNode? { + if !(inputPanelNode is ChatTextInputPanelNode) { + return nil + } switch chatPresentationInterfaceState.inputMode { case .media: if let currentNode = currentNode as? ChatMediaInputNode { diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index b3eb013e5a..ea2d42203b 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -282,7 +282,11 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> SynchronizeableChatInterfaceState { - return self.withUpdatedComposeInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId) + var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId) + if let timestamp = state?.timestamp { + result = result.withUpdatedTimestamp(timestamp) + } + return result } var effectiveInputState: ChatTextInputState { diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 20eb73f524..ee5fbfacdc 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -92,30 +92,30 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } if data.canReply { - actions.append(ContextMenuAction(content: .text("Reply"), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { interfaceInteraction.setupReplyMessage(message.id) })) } if data.canEdit { - actions.append(ContextMenuAction(content: .text("Edit"), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Edit), action: { interfaceInteraction.setupEditMessage(message.id) })) } - actions.append(ContextMenuAction(content: .text("Copy"), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), 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: { + if chatPresentationInterfaceState.pinnedMessage?.id != message.id { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Pin), action: { interfaceInteraction.pinMessage(message.id) })) } else { - actions.append(ContextMenuAction(content: .text("Unpin"), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_Unpin), action: { interfaceInteraction.unpinMessage() })) } @@ -124,7 +124,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: for media in message.media { if let file = media as? TelegramMediaFile { if file.isVideo && file.isAnimated { - actions.append(ContextMenuAction(content: .text("Save"), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_LinkDialogSave), action: { let _ = addSavedGif(postbox: account.postbox, file: file).start() })) break @@ -132,7 +132,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } - actions.append(ContextMenuAction(content: .text("More..."), action: { + actions.append(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuMore), action: { interfaceInteraction.beginMessageSelection(message.id) })) diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index 918c4c6e04..79ace2ffd8 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -27,7 +27,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation } return (inputQuery, signal |> then(stickers)) case let .hashtag(query): - /*var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if let currentQuery = currentQuery { switch currentQuery { case .hashtag: @@ -37,10 +37,18 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation } } - let hashtags: Signal = .single(.hashtags((0 ..< 3).map { "tag\($0)" })) + let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = recentlyUsedHashtags(postbox: account.postbox) |> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let normalizedQuery = query.lowercased() + var result: [String] = [] + for hashtag in hashtags { + if hashtag.lowercased().hasPrefix(normalizedQuery) { + result.append(hashtag) + } + } + return { _ in return .hashtags(result) } + } - return (inputQuery, signal |> then(hashtags))*/ - return (nil, .single({ _ in return nil })) + return (inputQuery, signal |> then(hashtags)) case let .mention(query): let normalizedQuery = query.lowercased() diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index a9135aa5e3..289d9c5667 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -4,14 +4,20 @@ import TelegramCore func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { if let _ = chatPresentationInterfaceState.search { - if let currentPanel = currentPanel as? ChatSearchInputPanelNode { - currentPanel.interfaceInteraction = interfaceInteraction - return currentPanel - } else { - let panel = ChatSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) - panel.account = account - panel.interfaceInteraction = interfaceInteraction - return panel + var hasSelection = false + if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState, !selectionState.selectedIds.isEmpty { + hasSelection = true + } + if !hasSelection { + if let currentPanel = currentPanel as? ChatSearchInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + return currentPanel + } else { + let panel = ChatSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } } } diff --git a/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 73d01eed52..7b4d14ff14 100644 --- a/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -12,27 +12,27 @@ struct ChatNavigationButton: Equatable { let buttonItem: UIBarButtonItem static func ==(lhs: ChatNavigationButton, rhs: ChatNavigationButton) -> Bool { - return lhs.action == rhs.action + return lhs.action == rhs.action && lhs.buttonItem === rhs.buttonItem } } -func leftNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { +func leftNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { if let _ = chatInterfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .clearHistory { return currentButton } else { - return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: "Delete All", style: .plain, target: target, action: selector)) + return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: strings.Conversation_ClearAll, style: .plain, target: target, action: selector)) } } return nil } -func rightNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { +func rightNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { if let _ = chatInterfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { return currentButton } else { - return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: "Cancel", style: .plain, target: target, action: selector)) + return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: target, action: selector)) } } diff --git a/TelegramUI/ChatInterfaceTitlePanelNodes.swift b/TelegramUI/ChatInterfaceTitlePanelNodes.swift index 70118bd1b9..fae2817ef6 100644 --- a/TelegramUI/ChatInterfaceTitlePanelNodes.swift +++ b/TelegramUI/ChatInterfaceTitlePanelNodes.swift @@ -7,7 +7,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat loop: for context in chatPresentationInterfaceState.titlePanelContexts.reversed() { switch context { case .pinnedMessage: - if chatPresentationInterfaceState.pinnedMessageId != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId { + if let pinnedMessage = chatPresentationInterfaceState.pinnedMessage, pinnedMessage.id != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId { selectedContext = context break loop } diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index f06e8218a1..7dc12c9620 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -290,22 +290,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { if let controllerInteraction = self.controllerInteraction, let currentMessage = self.currentMessage { var saveToCameraRoll: (() -> Void)? var shareAction: (([PeerId]) -> Void)? - let shareController = ShareController(account: self.account, shareAction: { peerIds in - shareAction?(peerIds) - }, defaultAction: ShareControllerAction(title: "Save to Camera Roll", action: { - saveToCameraRoll?() - })) + let shareController = ShareController(account: self.account, subject: .message(currentMessage), saveToCameraRoll: true) controllerInteraction.presentController(shareController, nil) - shareAction = { [weak shareController, weak self] peerIds in - shareController?.dismiss() - - if let strongSelf = self, let currentMessage = strongSelf.currentMessage { - for peerId in peerIds { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.forward(source: currentMessage.id)]).start() - } - } - } - saveToCameraRoll = { [weak shareController, weak self] in + /*saveToCameraRoll = { [weak shareController, weak self] in shareController?.dismiss() if let strongSelf = self, let currentMessage = strongSelf.currentMessage { @@ -462,6 +449,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { }) ])]) controllerInteraction.presentController(actionSheet, nil) + */ } } diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 865f551d06..1608a1e70b 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -292,8 +292,8 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD private func deactivateSearch(animated: Bool) { if !self.displayNavigationBar { - self.chatListDisplayNode.deactivateSearch(animated: animated) self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) + self.chatListDisplayNode.deactivateSearch(animated: animated) } } @@ -312,6 +312,13 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } let chatController = ChatController(account: self.account, peerId: peerId) + chatController.peekActions = .remove({ [weak self] in + if let strongSelf = self { + let _ = removeRecentPeer(account: strongSelf.account, peerId: peerId).start() + let searchContainer = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode + searchContainer?.removePeerFromTopPeers(peerId) + } + }) chatController.canReadHistory.set(false) chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) return chatController diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 2915324202..6572ecc60f 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -19,13 +19,14 @@ class ChatListItem: ListViewItem { let embeddedState: PeerChatListEmbeddedInterfaceState? let editing: Bool let hasActiveRevealControls: Bool + let inputActivities: [(Peer, PeerInputActivity)]? let interaction: ChatListNodeInteraction let selectable: Bool = true let header: ListViewItemHeader? - 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) { + 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, inputActivities: [(Peer, PeerInputActivity)]?, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { self.theme = theme self.strings = strings self.account = account @@ -38,6 +39,7 @@ class ChatListItem: ListViewItem { self.embeddedState = embeddedState self.editing = editing self.hasActiveRevealControls = hasActiveRevealControls + self.inputActivities = inputActivities self.header = header self.interaction = interaction } @@ -122,7 +124,7 @@ class ChatListItem: ListViewItem { } } -private let titleFont = Font.semibold(17.0) +private let titleFont = Font.medium(16.0) private let textFont = Font.regular(15.0) private let dateFont = Font.regular(14.0) private let badgeFont = Font.regular(14.0) @@ -159,11 +161,9 @@ private func revealOptions(strings: PresentationStrings, isPinned: Bool, isMuted return options } -private let peerMutedIcon = UIImage(bundleImageName: "Chat List/PeerMutedIcon")?.precomposed() - private let separatorHeight = 1.0 / UIScreen.main.scale -private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 24.0)! +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 26.0)! class ChatListItemNode: ItemListRevealOptionsItemNode { var item: ChatListItem? @@ -175,12 +175,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let titleNode: TextNode let authorNode: TextNode let textNode: TextNode + let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode let statusNode: ASImageNode let separatorNode: ASDisplayNode let badgeBackgroundNode: ASImageNode let badgeTextNode: TextNode let mentionBadgeNode: ASImageNode + var verificationIconNode: ASImageNode? let mutedIconNode: ASImageNode var editableControlNode: ItemListEditableControlNode? @@ -209,14 +211,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = true + //self.titleNode.contentMode = .topLeft + //self.titleNode.contentsScale = self.titleNode.contentsScaleForDisplay self.authorNode = TextNode() self.authorNode.isLayerBacked = true self.authorNode.displaysAsynchronously = true + //self.authorNode.contentMode = .topLeft + //self.authorNode.contentsScale = self.titleNode.contentsScaleForDisplay self.textNode = TextNode() self.textNode.isLayerBacked = true self.textNode.displaysAsynchronously = true + //self.textNode.contentMode = .topLeft + //self.textNode.contentsScale = self.titleNode.contentsScaleForDisplay + + self.inputActivitiesNode = ChatListInputActivitiesNode() + self.inputActivitiesNode.alpha = 0.0 self.dateNode = TextNode() self.dateNode.isLayerBacked = true @@ -331,6 +342,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textLayout = TextNode.asyncLayout(self.textNode) let titleLayout = TextNode.asyncLayout(self.titleNode) let authorLayout = TextNode.asyncLayout(self.authorNode) + let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout() let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) @@ -361,13 +373,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { 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 currentVerificationIconImage: UIImage? var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? @@ -380,129 +386,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - let peer: Peer? - - var hideAuthor = false - var messageText: String - if let message = message { - if let messageMain = messageMainPeer(message) { - peer = messageMain - } else { - peer = item.peer.chatMainPeer - } - - messageText = message.text - if message.text.isEmpty { - for media in message.media { - switch media { - case _ as TelegramMediaImage: - if message.text.isEmpty { - messageText = item.strings.Message_Photo - } - case let fileMedia as TelegramMediaFile: - if message.text.isEmpty { - if let fileName = fileMedia.fileName { - messageText = fileName - } else { - messageText = item.strings.Message_File - } - inner: for attribute in fileMedia.attributes { - switch attribute { - case .Animated: - messageText = item.strings.Message_Animation - break inner - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - messageText = item.strings.Message_Audio - break inner - } else { - let descriptionString: String - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - descriptionString = title + " — " + performer - } else if let title = title, !title.isEmpty { - descriptionString = title - } else if let performer = performer, !performer.isEmpty { - descriptionString = performer - } else if let fileName = fileMedia.fileName { - descriptionString = fileName - } else { - descriptionString = item.strings.Message_Audio - } - messageText = descriptionString - break inner - } - case let .Sticker(displayText, _, _): - if displayText.isEmpty { - messageText = item.strings.Message_Sticker - break inner - } else { - messageText = displayText + " " + item.strings.Message_Sticker - break inner - } - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - messageText = item.strings.Message_VideoMessage - } else { - messageText = item.strings.Message_Video - } - break inner - default: - break - } - } - } - case _ as TelegramMediaMap: - messageText = item.strings.Message_Location - case _ as TelegramMediaContact: - messageText = item.strings.Message_Contact - case let game as TelegramMediaGame: - messageText = "🎮 \(game.title)" - case let invoice as TelegramMediaInvoice: - messageText = invoice.title - case let action as TelegramMediaAction: - hideAuthor = true - switch action.action { - case .phoneCall: - if message.effectivelyIncoming { - messageText = item.strings.Notification_CallIncoming - } else { - messageText = item.strings.Notification_CallOutgoing - } - default: - if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { - messageText = text.string - } - } - case _ as TelegramMediaExpiredContent: - if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { - messageText = text.string - } - default: - break - } - } - } - } else { - peer = item.peer.chatMainPeer - messageText = "" - if item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { - if let secretChat = item.peer.peers[item.peer.peerId] as? TelegramSecretChat { - switch secretChat.embeddedState { - case .active: - messageText = item.strings.Notification_EncryptedChatAccepted - case .terminated: - messageText = item.strings.DialogList_EncryptionRejected - case .handshake: - switch secretChat.role { - case .creator: - messageText = item.strings.Notification_EncryptedChatRequested - case .participant: - messageText = item.strings.DialogList_EncryptionProcessing - } - } - } - } - } + let (peer, hideAuthor, messageText) = chatListItemStrings(strings: item.strings, message: item.message, chatPeer: item.peer, accountPeerId: item.account.peerId) let attributedText: NSAttributedString if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { @@ -567,17 +451,50 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + let tagSummaryCount = item.summaryInfo.tagSummaryCount ?? 0 + let actionsSummaryCount = item.summaryInfo.actionsSummaryCount ?? 0 + let totalMentionCount = tagSummaryCount - actionsSummaryCount + if totalMentionCount > 0 { + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.theme) + } else if item.index.pinningIndex != nil && currentBadgeBackgroundImage == nil { + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.theme) + } + if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { if case .muted = notificationSettings.muteState { - currentMutedIconImage = peerMutedIcon + currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.theme) } } let statusWidth = statusImage?.size.width ?? 0.0 - var muteWidth: CGFloat = 0.0 + var titleIconsWidth: CGFloat = 0.0 if let currentMutedIconImage = currentMutedIconImage { - muteWidth = currentMutedIconImage.size.width + 4.0 + if titleIconsWidth.isZero { + titleIconsWidth += 4.0 + } + titleIconsWidth += currentMutedIconImage.size.width + } + + var isVerified = false + if let peer = item.peer.chatMainPeer { + if let peer = peer as? TelegramUser { + isVerified = peer.flags.contains(.isVerified) + } else if let peer = peer as? TelegramChannel { + isVerified = peer.flags.contains(.isVerified) + } + } + + if isVerified { + currentVerificationIconImage = PresentationResourcesChatList.verifiedIcon(item.theme) + } + if let currentVerificationIconImage = currentVerificationIconImage { + if titleIconsWidth.isZero { + titleIconsWidth += 4.0 + } else { + titleIconsWidth += 2.0 + } + titleIconsWidth += currentVerificationIconImage.size.width } let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) @@ -602,8 +519,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (textLayout, textApply) = textLayout(textAttributedString, nil, authorAttributedString == nil ? 2 : 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)) - let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - muteWidth, height: rawContentRect.height)) + let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth, height: rawContentRect.height)) let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + var inputActivitiesSize: CGSize? + var inputActivitiesApply: (() -> Void)? + if let inputActivities = item.inputActivities, !inputActivities.isEmpty { + let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentRect.width - badgeSize, height: 40.0), item.strings, item.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities) + inputActivitiesSize = size + inputActivitiesApply = apply + } let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: itemHeight), insets: insets) @@ -718,19 +643,83 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mentionBadgeNode.isHidden = true } + var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + + if let currentVerificationIconImage = currentVerificationIconImage { + let iconNode: ASImageNode + if let current = strongSelf.verificationIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.addSubnode(iconNode) + strongSelf.verificationIconNode = iconNode + } + iconNode.image = currentVerificationIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 3.0), size: currentVerificationIconImage.size)) + nextTitleIconOrigin += currentVerificationIconImage.size.width + 5.0 + } else if let verificationIconNode = strongSelf.verificationIconNode { + strongSelf.verificationIconNode = nil + verificationIconNode.removeFromSupernode() + } + if let currentMutedIconImage = currentMutedIconImage { strongSelf.mutedIconNode.image = currentMutedIconImage strongSelf.mutedIconNode.isHidden = false - transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleLayout.size.width + 3.0, y: contentRect.origin.y + 6.0), size: currentMutedIconImage.size)) + transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 6.0), size: currentMutedIconImage.size)) + nextTitleIconOrigin += currentMutedIconImage.size.width + 3.0 } else { strongSelf.mutedIconNode.image = nil strongSelf.mutedIconNode.isHidden = true } let contentDeltaX = contentRect.origin.x - strongSelf.titleNode.frame.minX - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size) - strongSelf.authorNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0), size: authorLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) + let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) + strongSelf.authorNode.frame = authorNodeFrame + let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) + strongSelf.textNode.frame = textNodeFrame + + if let inputActivities = item.inputActivities, !inputActivities.isEmpty { + if strongSelf.inputActivitiesNode.supernode == nil { + strongSelf.addSubnode(strongSelf.inputActivitiesNode) + } + + if strongSelf.inputActivitiesNode.alpha.isZero { + strongSelf.inputActivitiesNode.alpha = 1.0 + strongSelf.textNode.alpha = 0.0 + strongSelf.authorNode.alpha = 0.0 + + if animated { + strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + } + } + } else { + if !strongSelf.inputActivitiesNode.alpha.isZero { + strongSelf.inputActivitiesNode.alpha = 0.0 + strongSelf.textNode.alpha = 1.0 + strongSelf.authorNode.alpha = 1.0 + if animated { + strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in + if let strongSelf = self, value { + strongSelf.inputActivitiesNode.removeFromSupernode() + } + }) + strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } else { + strongSelf.inputActivitiesNode.removeFromSupernode() + } + } + } + if let inputActivitiesSize = inputActivitiesSize { + strongSelf.inputActivitiesNode.frame = CGRect(origin: CGPoint(x: authorNodeFrame.minX + 1.0, y: authorNodeFrame.minY + UIScreenPixel), size: inputActivitiesSize) + } + inputActivitiesApply?() if !contentDeltaX.isZero { let titlePosition = strongSelf.titleNode.position @@ -821,14 +810,35 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let statusFrame = self.statusNode.frame transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: contentRect.origin.y + 5.0), size: statusFrame.size)) - let mutedIconFrame = self.mutedIconNode.frame - transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleFrame.size.width + 3.0, y: contentRect.origin.y + 6.0), size: mutedIconFrame.size)) + var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + if let verificationIconNode = self.verificationIconNode { + transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: verificationIconNode.frame.origin.y), size: verificationIconNode.bounds.size)) + nextTitleIconOrigin += verificationIconNode.bounds.size.width + 5.0 + } + + let mutedIconFrame = self.mutedIconNode.frame + transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 6.0), size: mutedIconFrame.size)) + nextTitleIconOrigin += mutedIconFrame.size.width + 3.0 let badgeBackgroundFrame = self.badgeBackgroundNode.frame let updatedBadgeBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeBackgroundFrame.size.width, y: contentRect.maxY - badgeBackgroundFrame.size.height - 2.0), size: badgeBackgroundFrame.size) transition.updateFrame(node: self.badgeBackgroundNode, frame: updatedBadgeBackgroundFrame) + if self.mentionBadgeNode.supernode != nil { + let mentionBadgeSize = self.mentionBadgeNode.bounds.size + let mentionBadgeOffset: CGFloat + if updatedBadgeBackgroundFrame.size.width.isZero { + mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width + } else { + mentionBadgeOffset = contentRect.maxX - updatedBadgeBackgroundFrame.size.width - 6.0 - mentionBadgeSize.width + } + + let badgeBackgroundWidth = mentionBadgeSize.width + let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: self.mentionBadgeNode.frame.origin.y, width: badgeBackgroundWidth, height: mentionBadgeSize.height) + transition.updateFrame(node: self.mentionBadgeNode, frame: badgeBackgroundFrame) + } + let badgeTextFrame = self.badgeTextNode.frame transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeBackgroundFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeTextFrame.size)) } @@ -847,8 +857,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } override func revealOptionSelected(_ option: ItemListRevealOption) { - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() + var close = true if let item = self.item { switch option.key { case RevealOptionKey.pin.rawValue: @@ -857,13 +866,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { item.interaction.setPeerPinned(item.index.messageIndex.id.peerId, false) case RevealOptionKey.mute.rawValue: item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, true) + close = false case RevealOptionKey.unmute.rawValue: item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false) + close = false case RevealOptionKey.delete.rawValue: item.interaction.deletePeer(item.index.messageIndex.id.peerId) default: break } } + if close { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } } } diff --git a/TelegramUI/ChatListItemStrings.swift b/TelegramUI/ChatListItemStrings.swift new file mode 100644 index 0000000000..a3b3e96073 --- /dev/null +++ b/TelegramUI/ChatListItemStrings.swift @@ -0,0 +1,134 @@ +import Foundation +import Postbox +import TelegramCore + +public func chatListItemStrings(strings: PresentationStrings, message: Message?, chatPeer: RenderedPeer, accountPeerId: PeerId) -> (peer: Peer?, hideAuthor: Bool, messageText: String) { + let peer: Peer? + + var hideAuthor = false + var messageText: String + if let message = message { + if let messageMain = messageMainPeer(message) { + peer = messageMain + } else { + peer = chatPeer.chatMainPeer + } + + messageText = message.text + if message.text.isEmpty { + for media in message.media { + switch media { + case _ as TelegramMediaImage: + if message.text.isEmpty { + messageText = strings.Message_Photo + } + case let fileMedia as TelegramMediaFile: + if message.text.isEmpty { + if let fileName = fileMedia.fileName { + messageText = fileName + } else { + messageText = strings.Message_File + } + var isAnimated = false + inner: for attribute in fileMedia.attributes { + switch attribute { + case .Animated: + isAnimated = true + break inner + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + messageText = strings.Message_Audio + break inner + } else { + let descriptionString: String + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + descriptionString = title + " — " + performer + } else if let title = title, !title.isEmpty { + descriptionString = title + } else if let performer = performer, !performer.isEmpty { + descriptionString = performer + } else if let fileName = fileMedia.fileName { + descriptionString = fileName + } else { + descriptionString = strings.Message_Audio + } + messageText = descriptionString + break inner + } + case let .Sticker(displayText, _, _): + if displayText.isEmpty { + messageText = strings.Message_Sticker + break inner + } else { + messageText = displayText + " " + strings.Message_Sticker + break inner + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + messageText = strings.Message_VideoMessage + } else { + messageText = strings.Message_Video + } + default: + break + } + } + if isAnimated { + messageText = strings.Message_Animation + } + } + case _ as TelegramMediaMap: + messageText = strings.Message_Location + case _ as TelegramMediaContact: + messageText = strings.Message_Contact + case let game as TelegramMediaGame: + messageText = "🎮 \(game.title)" + case let invoice as TelegramMediaInvoice: + messageText = invoice.title + case let action as TelegramMediaAction: + hideAuthor = true + switch action.action { + case .phoneCall: + if message.effectivelyIncoming { + messageText = strings.Notification_CallIncoming + } else { + messageText = strings.Notification_CallOutgoing + } + default: + if let text = plainServiceMessageString(strings: strings, message: message, accountPeerId: accountPeerId) { + messageText = text + } + } + case _ as TelegramMediaExpiredContent: + if let text = plainServiceMessageString(strings: strings, message: message, accountPeerId: accountPeerId) { + messageText = text + } + default: + break + } + } + } + } else { + peer = chatPeer.chatMainPeer + messageText = "" + if chatPeer.peerId.namespace == Namespaces.Peer.SecretChat { + if let secretChat = chatPeer.peers[chatPeer.peerId] as? TelegramSecretChat { + switch secretChat.embeddedState { + case .active: + messageText = strings.Notification_EncryptedChatAccepted + case .terminated: + messageText = strings.DialogList_EncryptionRejected + case .handshake: + switch secretChat.role { + case .creator: + messageText = strings.Notification_EncryptedChatRequested + case .participant: + messageText = strings.DialogList_EncryptionProcessing + } + } + } + } + } + + return (peer, hideAuthor, messageText) +} diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index a0764a252c..b09d352fcf 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -40,22 +40,35 @@ final class ChatListNodeInteraction { } } +final class ChatListNodePeerInputActivities { + let activities: [PeerId: [(Peer, PeerInputActivity)]] + + init(activities: [PeerId: [(Peer, PeerInputActivity)]]) { + self.activities = activities + } +} + struct ChatListNodeState: Equatable { let theme: PresentationTheme let strings: PresentationStrings let editing: Bool let peerIdWithRevealedOptions: PeerId? + let peerInputActivities: ChatListNodePeerInputActivities? func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> ChatListNodeState { - return ChatListNodeState(theme: theme, strings: strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) + return ChatListNodeState(theme: theme, strings: strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) } func withUpdatedEditing(_ editing: Bool) -> ChatListNodeState { - return ChatListNodeState(theme: self.theme, strings: self.strings, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) + return ChatListNodeState(theme: self.theme, strings: self.strings, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListNodeState { - return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions) + return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, peerInputActivities: self.peerInputActivities) + } + + func withUpdatedPeerInputActivities(_ peerInputActivities: ChatListNodePeerInputActivities?) -> ChatListNodeState { + return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, peerInputActivities: peerInputActivities) } static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { @@ -71,6 +84,9 @@ struct ChatListNodeState: Equatable { if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { return false } + if lhs.peerInputActivities !== rhs.peerInputActivities { + return false + } return true } } @@ -82,10 +98,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, summaryInfo, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): 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, summaryInfo: summaryInfo, 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, inputActivities: inputActivities, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? @@ -93,7 +109,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -112,10 +128,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, summaryInfo, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo, editing, hasActiveRevealControls, inputActivities): 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, summaryInfo: summaryInfo, 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, inputActivities: inputActivities, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? @@ -123,7 +139,7 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -159,6 +175,8 @@ final class ChatListNode: ListView { var deletePeerChat: ((PeerId) -> Void)? var presentAlert: ((String) -> Void)? + private var theme: PresentationTheme + private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? @@ -171,11 +189,14 @@ final class ChatListNode: ListView { private var currentLocation: ChatListNodeLocation? private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() + private var activityStatusesDisposable: Disposable? init(account: Account, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings) { - self.currentState = ChatListNodeState(theme: theme, strings: strings, editing: false, peerIdWithRevealedOptions: nil) + self.currentState = ChatListNodeState(theme: theme, strings: strings, editing: false, peerIdWithRevealedOptions: nil, peerInputActivities: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) + self.theme = theme + super.init() self.backgroundColor = theme.chatList.backgroundColor @@ -213,8 +234,12 @@ final class ChatListNode: ListView { } } }) - }, setPeerMuted: { peerId, _ in - let _ = togglePeerMuted(account: account, peerId: peerId).start() + }, setPeerMuted: { [weak self] peerId, _ in + let _ = togglePeerMuted(account: account, peerId: peerId).start(completed: { + self?.updateState { + return $0.withUpdatedPeerIdWithRevealedOptions(nil) + } + }) }, deletePeer: { [weak self] peerId in self?.deletePeerChat?(peerId) }) @@ -253,16 +278,20 @@ final class ChatListNode: ListView { prepareOnMainQueue = true } } else { - switch update.type { - case .InitialUnread: - reason = .initial - prepareOnMainQueue = true - case .Generic: - reason = .interactiveChanges - case .UpdateVisible: - reason = .reload - case .FillHole: - reason = .reload + if previous?.originalView === update.view { + reason = .interactiveChanges + } else { + switch update.type { + case .InitialUnread: + reason = .initial + prepareOnMainQueue = true + case .Generic: + reason = .interactiveChanges + case .UpdateVisible: + reason = .reload + case .FillHole: + reason = .reload + } } } @@ -299,16 +328,125 @@ final class ChatListNode: ListView { let initialLocation: ChatListNodeLocation = .initial(count: 50) self.currentLocation = initialLocation self.chatListLocation.set(initialLocation) + + let postbox = account.postbox + let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) + let previousActivities = Atomic(value: nil) + self.activityStatusesDisposable = (account.allPeerInputActivities() + |> mapToSignal { activitiesByPeerId -> Signal<[PeerId: [(Peer, PeerInputActivity)]], NoError> in + var foundAllPeers = true + var cachedResult: [PeerId: [(Peer, PeerInputActivity)]] = [:] + previousPeerCache.with { dict -> Void in + for (chatPeerId, activities) in activitiesByPeerId { + var cachedChatResult: [(Peer, PeerInputActivity)] = [] + for (peerId, activity) in activities { + if let peer = dict[peerId] { + cachedChatResult.append((peer, activity)) + } else { + foundAllPeers = false + break + } + cachedResult[chatPeerId] = cachedChatResult + } + } + } + if foundAllPeers { + return .single(cachedResult) + } else { + return postbox.modify { modifier -> [PeerId: [(Peer, PeerInputActivity)]] in + var result: [PeerId: [(Peer, PeerInputActivity)]] = [:] + var peerCache: [PeerId: Peer] = [:] + for (chatPeerId, activities) in activitiesByPeerId { + var chatResult: [(Peer, PeerInputActivity)] = [] + + for (peerId, activity) in activities { + if let peer = modifier.getPeer(peerId) { + chatResult.append((peer, activity)) + peerCache[peerId] = peer + } + } + + result[chatPeerId] = chatResult + } + let _ = previousPeerCache.swap(peerCache) + return result + } + } + } + |> map { activities -> ChatListNodePeerInputActivities? in + return previousActivities.modify { current in + var updated = false + let currentList: [PeerId: [(Peer, PeerInputActivity)]] = current?.activities ?? [:] + if currentList.count != activities.count { + updated = true + } else { + outer: for (peerId, currentValue) in currentList { + if let value = activities[peerId] { + if currentValue.count != value.count { + updated = true + break outer + } else { + for i in 0 ..< currentValue.count { + if !arePeersEqual(currentValue[i].0, value[i].0) { + updated = true + break outer + } + if currentValue[i].1 != value[i].1 { + updated = true + break outer + } + } + } + } else { + updated = true + break outer + } + } + } + if updated { + if activities.isEmpty { + return nil + } else { + return ChatListNodePeerInputActivities(activities: activities) + } + } else { + return current + } + } + } + |> deliverOnMainQueue).start(next: { [weak self] activities in + if let strongSelf = self { + strongSelf.updateState { current in + return current.withUpdatedPeerInputActivities(activities) + } + } + }) + + self.beganInteractiveDragging = { [weak self] in + if let strongSelf = self { + if strongSelf.currentState.peerIdWithRevealedOptions != nil { + strongSelf.updateState { + return $0.withUpdatedPeerIdWithRevealedOptions(nil) + } + } + } + } } deinit { self.chatListDisposable.dispose() + self.activityStatusesDisposable?.dispose() } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if theme !== self.currentState.theme || strings !== self.currentState.strings { + self.theme = theme self.backgroundColor = theme.chatList.backgroundColor + if self.keepTopItemOverscrollBackground != nil { + self.keepTopItemOverscrollBackground = theme.chatList.pinnedItemBackgroundColor + } + self.updateState { return $0.withUpdatedPresentationData(theme: theme, strings: strings) } @@ -358,6 +496,24 @@ final class ChatListNode: ListView { if let strongSelf = self { strongSelf.chatListView = transition.chatListView + var pinnedOverscroll = false + let entryCount = transition.chatListView.filteredEntries.count + if entryCount >= 2 { + if case .SearchEntry = transition.chatListView.filteredEntries[entryCount - 1] { + if transition.chatListView.filteredEntries[entryCount - 2].index.pinningIndex != nil { + pinnedOverscroll = true + } + } + } + + if pinnedOverscroll != (strongSelf.keepTopItemOverscrollBackground != nil) { + if pinnedOverscroll { + strongSelf.keepTopItemOverscrollBackground = strongSelf.theme.chatList.pinnedItemBackgroundColor + } else { + strongSelf.keepTopItemOverscrollBackground = nil + } + } + if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index bf8eca9507..e40b45040c 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, summaryInfo: ChatListMessageTagSummaryInfo, 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, inputActivities: [(Peer, PeerInputActivity)]?) 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, lhsSummaryInfo, lhsEditing, lhsHasRevealControls): + case let .PeerEntry(lhsIndex, lhsTheme, lhsStrings, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsInputActivities): switch rhs { - case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls): + case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsInputActivities): if lhsIndex != rhsIndex { return false } @@ -143,6 +143,21 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsSummaryInfo != rhsSummaryInfo { return false } + if let lhsInputActivities = lhsInputActivities, let rhsInputActivities = rhsInputActivities { + if lhsInputActivities.count != rhsInputActivities.count { + return false + } + for i in 0 ..< lhsInputActivities.count { + if !arePeersEqual(lhsInputActivities[i].0, rhsInputActivities[i].0) { + return false + } + if lhsInputActivities[i].1 != rhsInputActivities[i].1 { + return false + } + } + } else if (lhsInputActivities != nil) != (rhsInputActivities != nil) { + return false + } return true default: @@ -164,13 +179,13 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState) for entry in view.entries { switch entry { 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)) + 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, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId])) case let .HoleEntry(hole): result.append(.HoleEntry(hole, theme: state.theme)) } } if view.laterIndex == nil { - result.append(.SearchEntry(theme: state.theme, text: state.strings.ChatSearch_SearchPlaceholder)) + result.append(.SearchEntry(theme: state.theme, text: state.strings.DialogList_SearchLabel)) } return result } diff --git a/TelegramUI/ChatListRecentPeersListItem.swift b/TelegramUI/ChatListRecentPeersListItem.swift index ccea2d2f63..40178ca71c 100644 --- a/TelegramUI/ChatListRecentPeersListItem.swift +++ b/TelegramUI/ChatListRecentPeersListItem.swift @@ -113,6 +113,8 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } else { peersNode = ChatListSearchRecentPeersNode(account: item.account, theme: item.theme, strings: item.strings, peerSelected: { peer in self?.item?.peerSelected(peer) + }, isPeerSelected: { _ in + return false }) strongSelf.peersNode = peersNode strongSelf.addSubnode(peersNode) @@ -156,4 +158,8 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } return nil } + + func removePeer(_ peerId: PeerId) { + self.peersNode?.removePeer(peerId) + } } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 042499baa8..bf68976620 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Postbox import TelegramCore -enum ChatListRecentEntryStableId: Hashable { +private enum ChatListRecentEntryStableId: Hashable { case topPeers case peerId(PeerId) @@ -36,15 +36,15 @@ enum ChatListRecentEntryStableId: Hashable { } } -enum ChatListRecentEntry: Comparable, Identifiable { +private enum ChatListRecentEntry: Comparable, Identifiable { case topPeers([Peer], PresentationTheme, PresentationStrings) - case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationTheme, PresentationStrings) + case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationTheme, PresentationStrings, Bool) var stableId: ChatListRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _, _): + case let .peer(_, peer, _, _, _, _): return .peerId(peer.id) } } @@ -71,8 +71,8 @@ enum ChatListRecentEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsTheme, lhsStrings): - if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsTheme, lhsStrings, lhsHasRevealControls): + if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsTheme, rhsStrings, rhsHasRevealControls) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsHasRevealControls == rhsHasRevealControls { return true } else { return false @@ -84,23 +84,23 @@ enum ChatListRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _, _, _, _): + case let .peer(rhsIndex, _, _, _, _, _): return lhsIndex <= rhsIndex } } } - func item(account: Account, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { + func item(account: Account, peerSelected: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, account: account, peers: peers, peerSelected: { peer in peerSelected(peer) }) - case let .peer(_, peer, associatedPeer, theme, strings): + case let .peer(_, peer, associatedPeer, theme, strings, hasRevealControls): let primaryPeer: Peer var chatPeer: Peer? if let associatedPeer = associatedPeer { @@ -110,14 +110,15 @@ enum ChatListRecentEntry: Comparable, Identifiable { primaryPeer = peer chatPeer = associatedPeer } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: hasRevealControls, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { + clearRecentlySearchedPeers() + }), action: { _ in peerSelected(peer) - }) + }, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer) } } } - enum ChatListSearchEntryStableId: Hashable { case localPeerId(PeerId) case globalPeerId(PeerId) @@ -249,20 +250,20 @@ enum ChatListSearchEntry: Comparable, Identifiable { chatPeer = associatedPeer } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .addressName, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .addressName, selection: .none, hasActiveRevealControls: false, index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in 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, summaryInfo: ChatListMessageTagSummaryInfo(), 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, inputActivities: nil, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: theme, strings: strings, actionTitle: nil, action: nil) : nil, interaction: interaction) } } } -struct ChatListSearchContainerRecentTransition { +private struct ChatListSearchContainerRecentTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] @@ -275,12 +276,12 @@ struct ChatListSearchContainerTransition { let displayingResults: Bool } -func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, peerSelected: @escaping (Peer) -> Void) -> ChatListSearchContainerRecentTransition { +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], account: Account, peerSelected: @escaping (Peer) -> Void, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, peerSelected: peerSelected, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -295,6 +296,25 @@ func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearch return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) } +private struct ChatListSearchContainerNodeState: Equatable { + let peerIdWithRevealedOptions: PeerId? + + init(peerIdWithRevealedOptions: PeerId? = nil) { + self.peerIdWithRevealedOptions = peerIdWithRevealedOptions + } + + static func ==(lhs: ChatListSearchContainerNodeState, rhs: ChatListSearchContainerNodeState) -> Bool { + if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { + return false + } + return true + } + + func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListSearchContainerNodeState { + return ChatListSearchContainerNodeState(peerIdWithRevealedOptions: peerIdWithRevealedOptions) + } +} + final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openMessage: (Peer, MessageId) -> Void @@ -315,6 +335,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private var presentationDataDisposable: Disposable? private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + private var stateValue = ChatListSearchContainerNodeState() + private let statePromise: ValuePromise init(account: Account, openPeer: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { self.account = account @@ -323,16 +345,15 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) - //self.recentPeersNode = ChatListSearchRecentPeersNode(account: account, peerSelected: openPeer) self.recentListNode = ListView() - self.listNode = ListView() + self.statePromise = ValuePromise(self.stateValue, ignoreRepeated: true) + super.init() self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - //self.addSubnode(self.recentPeersNode) self.addSubnode(self.recentListNode) self.addSubnode(self.listNode) @@ -394,15 +415,25 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { openMessage(peer, message.id) } self?.listNode.clearHighlightAnimated(true) - }, setPeerIdWithRevealedOptions: { _, _ in + }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in + if let strongSelf = self { + strongSelf.updateState { state in + if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { + return state.withUpdatedPeerIdWithRevealedOptions(peerId) + } else { + return state + } + } + } }, setPeerPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in + }) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), themeAndStringsPromise.get()) - |> mapToSignal { [weak self] peers, themeAndStrings -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in + let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), themeAndStringsPromise.get(), self.statePromise.get()) + |> mapToSignal { [weak self] peers, themeAndStrings, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] entries.append(.topPeers([], themeAndStrings.0, themeAndStrings.1)) var peerIds = Set() @@ -418,7 +449,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if let associatedPeerId = peer.associatedPeerId { associatedPeer = renderedPeer.peers[associatedPeerId] } - entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, themeAndStrings.0, themeAndStrings.1)) + entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, themeAndStrings.0, themeAndStrings.1, state.peerIdWithRevealedOptions == peer.id)) index += 1 } } @@ -427,6 +458,14 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: account, peerSelected: { peer in self?.recentListNode.clearHighlightAnimated(true) openPeer(peer) + }, clearRecentlySearchedPeers: { + self?.clearRecentSearch() + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId) + }, deletePeer: { peerId in + if let strongSelf = self { + let _ = removeRecentlySearchedPeer(postbox: strongSelf.account.postbox, peerId: peerId).start() + } }) return .single((transition, previousEntries == nil)) } @@ -461,6 +500,14 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } }) + + self.recentListNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } } deinit { @@ -473,6 +520,14 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.backgroundColor = theme.chatList.backgroundColor } + private func updateState(_ f: (ChatListSearchContainerNodeState) -> ChatListSearchContainerNodeState) { + let state = f(self.stateValue) + if state != self.stateValue { + self.stateValue = state + self.statePromise.set(state) + } + } + override func searchTextUpdated(text: String) { if text.isEmpty { self.searchQuery.set(.single(nil)) @@ -496,9 +551,10 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.enqueuedRecentTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - options.insert(.PreferSynchronousDrawing) if firstTime { + options.insert(.PreferSynchronousDrawing) } else { + options.insert(.AnimateInsertion) } self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in @@ -542,10 +598,6 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - //let recentPeersSize = self.recentPeersNode.measure(CGSize(width: layout.size.width, height: CGFloat.infinity)) - //self.recentPeersNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: recentPeersSize) - //self.recentPeersNode.layout() - var duration: Double = 0.0 var curve: UInt = 0 switch transition { @@ -614,4 +666,16 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } return nil } + + private func clearRecentSearch() { + let _ = (clearRecentlySearchedPeers(postbox: self.account.postbox) |> deliverOnMainQueue).start() + } + + func removePeerFromTopPeers(_ peerId: PeerId) { + self.recentListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatListRecentPeersListItemNode { + itemNode.removePeer(peerId) + } + } + } } diff --git a/TelegramUI/ChatListSearchItem.swift b/TelegramUI/ChatListSearchItem.swift index 57dd7b6e41..8bf0f1b17d 100644 --- a/TelegramUI/ChatListSearchItem.swift +++ b/TelegramUI/ChatListSearchItem.swift @@ -5,7 +5,7 @@ import Postbox import Display import SwiftSignalKit -private let searchBarFont = Font.regular(15.0) +private let searchBarFont = Font.regular(14.0) class ChatListSearchItem: ListViewItem { let selectable: Bool = false @@ -100,7 +100,7 @@ class ChatListSearchItemNode: ListViewItemNode { let placeholder = self.placeholder return { item, width, nextIsPinned in - let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, nextIsPinned ? item.theme.chatList.itemBackgroundColor : item.theme.chatList.pinnedItemBackgroundColor) + let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), UIColor(rgb: 0x8e8e93), nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, nextIsPinned ? item.theme.chatList.itemBackgroundColor : item.theme.chatList.pinnedItemBackgroundColor) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 44.0 + 4.0), insets: UIEdgeInsets()) diff --git a/TelegramUI/ChatListSearchItemHeader.swift b/TelegramUI/ChatListSearchItemHeader.swift index c597c378cb..0f5f992631 100644 --- a/TelegramUI/ChatListSearchItemHeader.swift +++ b/TelegramUI/ChatListSearchItemHeader.swift @@ -16,18 +16,22 @@ final class ChatListSearchItemHeader: ListViewItemHeader { let stickDirection: ListViewItemHeaderStickDirection = .top let theme: PresentationTheme let strings: PresentationStrings + let actionTitle: String? + let action: (() -> Void)? let height: CGFloat = 29.0 - init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings) { + init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String?, action: (() -> Void)?) { self.type = type self.id = Int64(self.type.rawValue) self.theme = theme self.strings = strings + self.actionTitle = actionTitle + self.action = action } func node() -> ListViewItemHeaderNode { - return ChatListSearchItemHeaderNode(type: self.type, theme: self.theme, strings: self.strings) + return ChatListSearchItemHeaderNode(type: self.type, theme: self.theme, strings: self.strings, actionTitle: self.actionTitle, action: self.action) } } @@ -35,13 +39,17 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { private let type: ChatListSearchItemHeaderType private var theme: PresentationTheme private var strings: PresentationStrings + private let actionTitle: String? + private let action: (() -> Void)? private let sectionHeaderNode: ListSectionHeaderNode - init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings) { + init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String?, action: (() -> Void)?) { self.type = type self.theme = theme self.strings = strings + self.actionTitle = actionTitle + self.action = action self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) @@ -62,10 +70,18 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased() } + self.sectionHeaderNode.action = actionTitle + self.sectionHeaderNode.activateAction = action + self.addSubnode(self.sectionHeaderNode) } override func layout() { self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) } + + override func animateRemoved(duration: Double) { + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + } } diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 40d7dda1c9..b0510aaf51 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -5,20 +5,48 @@ import SwiftSignalKit import Postbox import TelegramCore +private func calculateItemCustomWidth(width: CGFloat) -> CGFloat { + let itemInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 6.0) + let minimalItemWidth: CGFloat = width > 301.0 ? 70.0 : 60.0 + let effectiveWidth1 = width - 12.0 - 12.0 + let effectiveWidth = width - itemInsets.left - itemInsets.right + + let itemsPerRow = Int(effectiveWidth1 / minimalItemWidth) + let itemWidth = floor(effectiveWidth1 / CGFloat(itemsPerRow)) + + let itemsInRow = Int(effectiveWidth / itemWidth) + let itemsInRowWidth = CGFloat(itemsInRow) * itemWidth + let remainingWidth = max(0.0, effectiveWidth - itemsInRowWidth) + + let itemSpacing = floorToScreenPixels(remainingWidth / CGFloat(itemsInRow + 1)) + + return itemWidth + itemSpacing +} + final class ChatListSearchRecentPeersNode: ASDisplayNode { private var theme: PresentationTheme private var strings: PresentationStrings private let sectionHeaderNode: ListSectionHeaderNode private let listView: ListView + private let share: Bool + + private let peerSelected: (Peer) -> Void + private let isPeerSelected: (PeerId) -> Bool private let disposable = MetaDisposable() - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) { + private var items: [ListViewItem] = [] + private var itemCustomWidth: CGFloat? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false) { self.theme = theme self.strings = strings + self.share = share + self.peerSelected = peerSelected + self.isPeerSelected = isPeerSelected self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) - self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople + self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople.uppercased() self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -32,8 +60,9 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { if let strongSelf = self { var items: [ListViewItem] = [] for peer in peers { - items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, account: account, peer: peer, action: peerSelected)) + items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, account: account, peer: peer, action: peerSelected, isPeerSelected: isPeerSelected, customWidth: strongSelf.itemCustomWidth)) } + strongSelf.items = items strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil) } })) @@ -65,9 +94,31 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.size.width, height: 29.0)) self.sectionHeaderNode.layout() + var insets = UIEdgeInsets() + + var itemCustomWidth: CGFloat? + if self.share { + insets.top = 7.0 + insets.bottom = 7.0 + + itemCustomWidth = calculateItemCustomWidth(width: bounds.size.width) + } + + var updateItems: [ListViewUpdateItem] = [] + if itemCustomWidth != self.itemCustomWidth { + self.itemCustomWidth = itemCustomWidth + + for i in 0 ..< self.items.count { + if let item = self.items[i] as? HorizontalPeerItem { + self.items[i] = HorizontalPeerItem(theme: self.theme, strings: self.strings, account: item.account, peer: item.peer, action: self.peerSelected, isPeerSelected: self.isPeerSelected, customWidth: itemCustomWidth) + updateItems.append(ListViewUpdateItem(index: i, previousIndex: i, item: self.items[i], directionHint: nil)) + } + } + } + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: bounds.size.width) self.listView.position = CGPoint(x: bounds.size.width / 2.0, y: 92.0 / 2.0 + 29.0) - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: bounds.size.width), insets: UIEdgeInsets(), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: updateItems, options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: bounds.size.width), insets: insets, duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, PeerId)? { @@ -78,9 +129,27 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { selectedItemNode = itemNode } } - if let selectedItemNode = selectedItemNode as? HorizontalPeerItemNode, let peer = selectedItemNode.peer { + if let selectedItemNode = selectedItemNode as? HorizontalPeerItemNode, let peer = selectedItemNode.item?.peer { return (selectedItemNode.view, peer.id) } return nil } + + func removePeer(_ peerId: PeerId) { + for i in 0 ..< self.items.count { + if let item = self.items[i] as? HorizontalPeerItem, item.peer.id == peerId { + self.items.remove(at: i) + self.listView.transaction(deleteIndices: [ListViewDeleteItem(index: i, directionHint: nil)], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.AnimateInsertion], updateOpaqueState: nil) + break + } + } + } + + func updateSelectedPeers(animated: Bool) { + self.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? HorizontalPeerItemNode { + itemNode.updateSelection(animated: animated) + } + } + } } diff --git a/TelegramUI/ChatListTypingNode.swift b/TelegramUI/ChatListTypingNode.swift new file mode 100644 index 0000000000..1a52d45aa8 --- /dev/null +++ b/TelegramUI/ChatListTypingNode.swift @@ -0,0 +1,150 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import Display +import SwiftSignalKit + +private let textFont = Font.regular(15.0) + +private func generateDotsImage(color: UIColor) -> UIImage? { + var images: [UIImage] = [] + let size = CGSize(width: 20.0, height: 10.0) + for i in 0 ..< 4 { + if let image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + var string = "" + if i >= 1 { + for _ in 1 ... i { + string.append(".") + } + } + let attributedString = NSAttributedString(string: string, attributes: [.font: textFont, .foregroundColor: color]) + UIGraphicsPushContext(context) + attributedString.draw(at: CGPoint(x: 1.0, y: -9.0)) + UIGraphicsPopContext() + }) { + images.append(image) + } + } + return UIImage.animatedImage(with: images, duration: 0.6) +} + +private final class ChatListInputActivitiesDotsNode: ASDisplayNode { + var image: UIImage? { + didSet { + if self.image !== oldValue { + if self.image != nil && self.isInHierarchy { + self.beginAnimation() + } else { + self.layer.removeAnimation(forKey: "image") + } + } + } + } + + override init() { + super.init() + + self.isLayerBacked = true + } + + private func beginAnimation() { + guard let images = self.image?.images else { + return + } + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.values = images.map { $0.cgImage! } + animation.duration = 0.54 + animation.repeatCount = Float.infinity + animation.calculationMode = kCAAnimationDiscrete + self.layer.add(animation, forKey: "image") + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + self.beginAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.layer.removeAnimation(forKey: "image") + } +} + +private let cachedImages = Atomic<[UInt32: UIImage]>(value: [:]) +private func getDotsImage(color: UIColor) -> UIImage? { + let key = color.argb + let cached = cachedImages.with { dict -> UIImage? in + return dict[key] + } + if let cached = cached { + return cached + } else if let image = generateDotsImage(color: color) { + let _ = cachedImages.modify { dict in + var dict = dict + dict[key] = image + return dict + } + return image + } else { + return nil + } +} + +final class ChatListInputActivitiesNode: ASDisplayNode { + private let textNode: TextNode + private let dotsNode: ChatListInputActivitiesDotsNode + + override init() { + self.textNode = TextNode() + self.textNode.isLayerBacked = true + + self.dotsNode = ChatListInputActivitiesDotsNode() + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.dotsNode) + } + + func asyncLayout() -> (CGSize, PresentationStrings, UIColor, PeerId, [(Peer, PeerInputActivity)]) -> (CGSize, () -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + return { [weak self] boundingSize, strings, color, peerId, activities in + let string: NSAttributedString? + if !activities.isEmpty { + if activities.count == 1 { + if activities[0].0.id == peerId { + string = NSAttributedString(string: strings.DialogList_Typing, font: textFont, textColor: color) + } else { + string = NSAttributedString(string: strings.DialogList_SingleTypingSuffix(activities[0].0.compactDisplayTitle).0, font: textFont, textColor: color) + } + } else { + string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color) + } + } else { + string = nil + } + let (textLayout, textApply) = makeTextLayout(string, nil, 1, .end, CGSize(width: boundingSize.width - 12.0, height: boundingSize.height), .left, nil, UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)) + + let dots = getDotsImage(color: color) + + return (boundingSize, { + if let strongSelf = self { + let _ = textApply() + strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: textLayout.size) + + if let dots = dots { + strongSelf.dotsNode.image = dots + let dotsSize = CGSize(width: 20.0, height: 10.0) + let dotsFrame = CGRect(origin: CGPoint(x: textLayout.size.width - 1.0, y: textLayout.size.height - dotsSize.height - 2.0), size: dotsSize) + strongSelf.dotsNode.frame = dotsFrame + } + } + }) + } + } +} diff --git a/TelegramUI/ChatLoadingNode.swift b/TelegramUI/ChatLoadingNode.swift new file mode 100644 index 0000000000..abbb2390fa --- /dev/null +++ b/TelegramUI/ChatLoadingNode.swift @@ -0,0 +1,35 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ChatLoadingNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let activityIndicator: ActivityIndicator + + init(theme: PresentationTheme) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = PresentationResourcesChat.chatLoadingIndicatorBackgroundImage(theme) + + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor), speed: .regular) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.activityIndicator) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) + + if let image = self.backgroundNode.image { + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - image.size.width) / 2.0), y: displayRect.minY + floor((displayRect.height - image.size.height) / 2.0)), size: image.size)) + } + + let activitySize = self.activityIndicator.measure(size) + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0)), size: activitySize)) + } +} + diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 61952faf9a..3c9dd94fe9 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -6,52 +6,20 @@ import TelegramCore import SwiftSignalKit final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { - private let multiplexedNode: MultiplexedVideoNode + private let account: Account + private let controllerInteraction: ChatControllerInteraction + + private var multiplexedNode: MultiplexedVideoNode? private let disposable = MetaDisposable() + private var validLayout: CGSize? + init(account: Account, controllerInteraction: ChatControllerInteraction) { - self.multiplexedNode = MultiplexedVideoNode(account: account) + self.account = account + self.controllerInteraction = controllerInteraction super.init() - - self.view.addSubview(self.multiplexedNode) - let initialOrder = Atomic<[MediaId]?>(value: nil) - let gifs = account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) - |> map { view -> [TelegramMediaFile] in - var recentGifs: OrderedItemListView? - if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { - recentGifs = orderedView as? OrderedItemListView - } - if let recentGifs = recentGifs { - return recentGifs.items.map { ($0.contents as! RecentMediaItem).media as! TelegramMediaFile } - } else { - return [] - } - } - self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in - if let strongSelf = self { - strongSelf.multiplexedNode.files = gifs - } - })) - - self.multiplexedNode.fileSelected = { file in - controllerInteraction.sendGif(file) - } - self.multiplexedNode.fileLongPressed = { [weak self] file in - if let strongSelf = self, let itemFrame = strongSelf.multiplexedNode.frameForItem(file.fileId) { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Delete"), action: { - let _ = removeSavedGif(postbox: account.postbox, mediaId: file.fileId).start() - })]) - controllerInteraction.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { - if let strongSelf = self { - return (strongSelf, strongSelf.multiplexedNode.convert(itemFrame, to: strongSelf.view).insetBy(dx: -2.0, dy: -2.0).offsetBy(dx: strongSelf.multiplexedNode.frame.minX, dy: strongSelf.multiplexedNode.frame.minY)) - } else { - return nil - } - })) - } - } } deinit { @@ -59,6 +27,60 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.multiplexedNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + self.validLayout = size + self.multiplexedNode?.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + if self.multiplexedNode == nil { + let multiplexedNode = MultiplexedVideoNode(account: account) + self.multiplexedNode = multiplexedNode + if let validLayout = self.validLayout { + multiplexedNode.frame = CGRect(origin: CGPoint(), size: validLayout) + } + + self.view.addSubview(multiplexedNode) + let initialOrder = Atomic<[MediaId]?>(value: nil) + let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) + |> map { view -> [TelegramMediaFile] in + var recentGifs: OrderedItemListView? + if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { + recentGifs = orderedView as? OrderedItemListView + } + if let recentGifs = recentGifs { + return recentGifs.items.map { ($0.contents as! RecentMediaItem).media as! TelegramMediaFile } + } else { + return [] + } + } + self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in + if let strongSelf = self { + strongSelf.multiplexedNode?.files = gifs + } + })) + + multiplexedNode.fileSelected = { [weak self] file in + self?.controllerInteraction.sendGif(file) + } + multiplexedNode.fileLongPressed = { [weak self] file in + if let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode, let itemFrame = multiplexedNode.frameForItem(file.fileId) { + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Common_Delete), action: { + if let strongSelf = self { + let _ = removeSavedGif(postbox: strongSelf.account.postbox, mediaId: file.fileId).start() + } + })]) + strongSelf.controllerInteraction.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { + if let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode { + return (strongSelf, multiplexedNode.convert(itemFrame, to: strongSelf.view).insetBy(dx: -2.0, dy: -2.0).offsetBy(dx: multiplexedNode.frame.minX, dy: multiplexedNode.frame.minY)) + } else { + return nil + } + })) + } + } + } } } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 0ce882870e..f6ead05791 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -52,7 +52,7 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr index += 1 } stationaryItems = .indices(indices) - case let .navigate(index): + case let .navigate(index, collectionId): if let index = index { for i in 0 ..< toEntries.count { if toEntries[i].index >= index { @@ -65,7 +65,21 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr } } } else if !toEntries.isEmpty { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true) + if let collectionId = collectionId { + for i in 0 ..< toEntries.count { + if toEntries[i].index.collectionId == collectionId { + var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up + if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { + directionHint = .down + } + scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true) + break + } + } + } + if scrollToItem == nil { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true) + } } } @@ -112,10 +126,12 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: } } + var savedStickerIds = Set() 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 { + savedStickerIds.insert(item.file.fileId.id) 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)) @@ -125,11 +141,18 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: if let recentStickers = recentStickers, !recentStickers.items.isEmpty { 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) { + var addedCount = 0 + for i in 0 ..< recentStickers.items.count { + if addedCount >= 20 { + break + } 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) - let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + if !savedStickerIds.contains(mediaId.id) { + let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) + let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) + entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + addedCount += 1 + } } } } @@ -145,7 +168,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: private enum StickerPacksCollectionPosition: Equatable { case initial case scroll(aroundIndex: ItemCollectionViewEntryIndex?) - case navigate(index: ItemCollectionViewEntryIndex?) + case navigate(index: ItemCollectionViewEntryIndex?, collectionId: ItemCollectionId?) static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool { switch lhs { @@ -170,7 +193,7 @@ private enum StickerPacksCollectionPosition: Equatable { private enum StickerPacksCollectionUpdate { case generic case scroll - case navigate(ItemCollectionViewEntryIndex?) + case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) } final class ChatMediaInputNodeInteraction { @@ -280,17 +303,17 @@ final class ChatMediaInputNode: ChatInputNode { 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))) + strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil))) + strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) for (id, _, _) in currentView.collectionInfos { if id.namespace == collectionId.namespace { if id == collectionId { let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex))) + strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil))) break } index += 1 @@ -329,14 +352,14 @@ final class ChatMediaInputNode: ChatInputNode { } return (view, update) } - case let .navigate(index): + case let .navigate(index, collectionId): var firstTime = true 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 { firstTime = false - update = .navigate(index) + update = .navigate(index, collectionId) } else { update = .generic } diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index f6e0fa7189..78884205e0 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -8,339 +8,352 @@ import TelegramCore private let titleFont = Font.regular(13.0) private let titleBoldFont = Font.bold(13.0) -private func peerMentionAttributes(theme: PresentationThemeServiceMessage, peerId: PeerId) -> MarkdownAttributeSet { - return MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [TextNode.TelegramPeerMentionAttribute: TelegramPeerMention(peerId: peerId, mention: "")]) +private func peerMentionAttributes(primaryTextColor: UIColor, peerId: PeerId) -> MarkdownAttributeSet { + return MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [TextNode.TelegramPeerMentionAttribute: TelegramPeerMention(peerId: peerId, mention: "")]) } -private func peerMentionsAttributes(theme: PresentationThemeServiceMessage, peerIds: [(Int, PeerId?)]) -> [Int: MarkdownAttributeSet] { +private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, PeerId?)]) -> [Int: MarkdownAttributeSet] { var result: [Int: MarkdownAttributeSet] = [:] for (index, peerId) in peerIds { if let peerId = peerId { - result[index] = peerMentionAttributes(theme: theme, peerId: peerId) + result[index] = peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: peerId) } } return result } -func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? { +private func attributedServiceMessageString(theme: PresentationTheme, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? { + return universalServiceMessageString(theme: theme, strings: strings, message: message, accountPeerId: accountPeerId) +} + +func plainServiceMessageString(strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> String? { + return universalServiceMessageString(theme: nil, strings: strings, message: message, accountPeerId: accountPeerId)?.string +} + +private func universalServiceMessageString(theme: PresentationTheme?, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? { var attributedString: NSAttributedString? - let theme = theme.chat.serviceMessage + let theme = theme?.chat.serviceMessage - let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) + let primaryTextColor = theme?.serviceMessagePrimaryTextColor ?? UIColor.black + + let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [:]) for media in message.media { if let action = media as? TelegramMediaAction { let authorName = message.author?.displayTitle ?? "" - var isChannel = false if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isChannel = true } switch action.action { - case .groupCreated: - if isChannel { - attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } else { - attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } - case let .addedMembers(peerIds): - if let peerId = peerIds.first, peerId == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, peerId)])) - } else { - var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] - if peerIds.count == 1 { - attributePeerIds.append((1, peerIds.first)) - } - attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: attributePeerIds)) - } - case let .removedMembers(peerIds): - if peerIds.first == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - } else { - var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] - if peerIds.count == 1 { - attributePeerIds.append((1, peerIds.first)) - } - attributedString = addAttributesToStringWithRanges(strings.Notification_Kicked(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: attributePeerIds)) - } - case let .photoUpdated(image): - if authorName.isEmpty || isChannel { + case .groupCreated: if isChannel { - if image != nil { - attributedString = NSAttributedString(string: strings.Channel_MessagePhotoUpdated, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } else { - attributedString = NSAttributedString(string: strings.Channel_MessagePhotoRemoved, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: primaryTextColor) + } + case let .addedMembers(peerIds): + if let peerId = peerIds.first, peerId == message.author?.id { + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)])) + } else { + var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + if peerIds.count == 1 { + attributePeerIds.append((1, peerIds.first)) } + attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) + } + case let .removedMembers(peerIds): + if peerIds.first == message.author?.id { + attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) } else { - if image != nil { - attributedString = NSAttributedString(string: strings.Group_MessagePhotoUpdated, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } else { - attributedString = NSAttributedString(string: strings.Group_MessagePhotoRemoved, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + if peerIds.count == 1 { + attributePeerIds.append((1, peerIds.first)) } + attributedString = addAttributesToStringWithRanges(strings.Notification_Kicked(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) } - } else { - if image != nil { - attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_RemovedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - } - } - case let .titleUpdated(title): - if authorName.isEmpty || isChannel { - if isChannel { - attributedString = NSAttributedString(string: strings.Channel_MessageTitleUpdated(title).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } else { - attributedString = NSAttributedString(string: strings.Group_MessageTitleUpdated(title).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } - } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - } - case .pinnedMessageUpdated: - enum PinnnedMediaType { - case text(String) - case photo - case video - case round - case audio - case file - case gif - case sticker - case location - case contact - case deleted - } - - var pinnedMessage: Message? - for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { - pinnedMessage = message - } - } - - var type: PinnnedMediaType - if let pinnedMessage = pinnedMessage { - type = .text(pinnedMessage.text) - inner: for media in pinnedMessage.media { - if let _ = media as? TelegramMediaImage { - type = .photo - } else if let file = media as? TelegramMediaFile { - type = .file - if file.isAnimated { - type = .gif + case let .photoUpdated(image): + if authorName.isEmpty || isChannel { + if isChannel { + if image != nil { + attributedString = NSAttributedString(string: strings.Channel_MessagePhotoUpdated, font: titleFont, textColor: primaryTextColor) } else { - for attribute in file.attributes { - switch attribute { - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - type = .round - } else { - type = .video - } - break inner - case let .Audio(isVoice, _, performer, title, _): - if isVoice { - type = .audio - } else { - let descriptionString: String - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - descriptionString = title + " — " + performer - } else if let title = title, !title.isEmpty { - descriptionString = title - } else if let performer = performer, !performer.isEmpty { - descriptionString = performer - } else if let fileName = file.fileName { - descriptionString = fileName + attributedString = NSAttributedString(string: strings.Channel_MessagePhotoRemoved, font: titleFont, textColor: primaryTextColor) + } + } else { + if image != nil { + attributedString = NSAttributedString(string: strings.Group_MessagePhotoUpdated, font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Group_MessagePhotoRemoved, font: titleFont, textColor: primaryTextColor) + } + } + } else { + if image != nil { + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_RemovedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } + } + case let .titleUpdated(title): + if authorName.isEmpty || isChannel { + if isChannel { + attributedString = NSAttributedString(string: strings.Channel_MessageTitleUpdated(title).0, font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Group_MessageTitleUpdated(title).0, font: titleFont, textColor: primaryTextColor) + } + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } + case .pinnedMessageUpdated: + enum PinnnedMediaType { + case text(String) + case photo + case video + case round + case audio + case file + case gif + case sticker + case location + case contact + case deleted + } + + var pinnedMessage: Message? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + pinnedMessage = message + } + } + + var type: PinnnedMediaType + if let pinnedMessage = pinnedMessage { + type = .text(pinnedMessage.text) + inner: for media in pinnedMessage.media { + if let _ = media as? TelegramMediaImage { + type = .photo + } else if let file = media as? TelegramMediaFile { + type = .file + if file.isAnimated { + type = .gif + } else { + for attribute in file.attributes { + switch attribute { + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + type = .round } else { - descriptionString = strings.Message_Audio + type = .video } - type = .text(descriptionString) + break inner + case let .Audio(isVoice, _, performer, title, _): + if isVoice { + type = .audio + } else { + let descriptionString: String + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + descriptionString = title + " — " + performer + } else if let title = title, !title.isEmpty { + descriptionString = title + } else if let performer = performer, !performer.isEmpty { + descriptionString = performer + } else if let fileName = file.fileName { + descriptionString = fileName + } else { + descriptionString = strings.Message_Audio + } + type = .text(descriptionString) + } + break inner + case .Sticker: + type = .sticker + break inner + case .Animated: + break + default: + break } - break inner - case .Sticker: - type = .sticker - break inner - case .Animated: - break - default: - break } } + } else if let _ = media as? TelegramMediaMap { + type = .location + } else if let _ = media as? TelegramMediaContact { + type = .contact } - } else if let _ = media as? TelegramMediaMap { - type = .location - } else if let _ = media as? TelegramMediaContact { - type = .contact } + } else { + type = .deleted } - } else { - type = .deleted - } - - switch type { - case let .text(text): - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedTextMessage(authorName, text.replacingOccurrences(of: "\n", with: " ")), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .photo: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPhotoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .video: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedVideoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .round: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedRoundMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .audio: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAudioMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .file: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDocumentMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .gif: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAnimationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .sticker: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedStickerMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .location: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .contact: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .deleted: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDeletedMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - } - case .joinedByLink: - attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedGroupByLink(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) - case .channelMigratedFromGroup, .groupMigratedToChannel: - attributedString = NSAttributedString(string: strings.Notification_ChannelMigratedFrom, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - case let .messageAutoremoveTimeoutUpdated(timeout): - if timeout > 0 { - let timeValue = timeIntervalString(strings: strings, value: timeout) - let string: String - if message.author?.id == accountPeerId { - string = strings.Notification_MessageLifetimeChangedOutgoing(timeValue).0 - } else { - let authorString: String - if let author = messageMainPeer(message) { - authorString = author.compactDisplayTitle - } else { - authorString = "" - } - string = strings.Notification_MessageLifetimeChanged(authorString, timeValue).0 + switch type { + case let .text(text): + var clippedText = text.replacingOccurrences(of: "\n", with: " ") + if clippedText.count > 14 { + clippedText = "\(clippedText[...clippedText.index(clippedText.startIndex, offsetBy: 14)])..." + } + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedTextMessage(authorName, clippedText), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .photo: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPhotoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .video: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedVideoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .round: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedRoundMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .audio: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAudioMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .file: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDocumentMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .gif: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAnimationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .sticker: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedStickerMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .location: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .contact: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .deleted: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDeletedMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) } - attributedString = NSAttributedString(string: string, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } else { - let string: String - if message.author?.id == accountPeerId { - string = strings.Notification_MessageLifetimeRemovedOutgoing - } else { - let authorString: String - if let author = messageMainPeer(message) { - authorString = author.compactDisplayTitle + case .joinedByLink: + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedGroupByLink(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .channelMigratedFromGroup, .groupMigratedToChannel: + attributedString = NSAttributedString(string: strings.Notification_ChannelMigratedFrom, font: titleFont, textColor: primaryTextColor) + case let .messageAutoremoveTimeoutUpdated(timeout): + if timeout > 0 { + let timeValue = timeIntervalString(strings: strings, value: timeout) + + let string: String + if message.author?.id == accountPeerId { + string = strings.Notification_MessageLifetimeChangedOutgoing(timeValue).0 } else { - authorString = "" + let authorString: String + if let author = messageMainPeer(message) { + authorString = author.compactDisplayTitle + } else { + authorString = "" + } + string = strings.Notification_MessageLifetimeChanged(authorString, timeValue).0 } - string = strings.Notification_MessageLifetimeRemoved(authorString).0 + attributedString = NSAttributedString(string: string, font: titleFont, textColor: primaryTextColor) + } else { + let string: String + if message.author?.id == accountPeerId { + string = strings.Notification_MessageLifetimeRemovedOutgoing + } else { + let authorString: String + if let author = messageMainPeer(message) { + authorString = author.compactDisplayTitle + } else { + authorString = "" + } + string = strings.Notification_MessageLifetimeRemoved(authorString).0 + } + attributedString = NSAttributedString(string: string, font: titleFont, textColor: primaryTextColor) } - attributedString = NSAttributedString(string: string, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } - case .historyCleared: - break - case .historyScreenshot: - attributedString = NSAttributedString(string: strings.Notification_SecretChatScreenshot, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - case let .gameScore(gameId: _, score): - var gameTitle: String? - inner: for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { - for media in message.media { - if let game = media as? TelegramMediaGame { - gameTitle = game.title - break inner + case .historyCleared: + break + case .historyScreenshot: + attributedString = NSAttributedString(string: strings.Notification_SecretChatScreenshot, font: titleFont, textColor: primaryTextColor) + case let .gameScore(gameId: _, score): + var gameTitle: String? + inner: for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + for media in message.media { + if let game = media as? TelegramMediaGame { + gameTitle = game.title + break inner + } } } } - } - - var baseString: String - if message.author?.id == accountPeerId { - if let _ = gameTitle { - baseString = strings.ServiceMessage_GameScoreSelfExtended(score) + + var baseString: String + if message.author?.id == accountPeerId { + if let _ = gameTitle { + baseString = strings.ServiceMessage_GameScoreSelfExtended(score) + } else { + baseString = strings.ServiceMessage_GameScoreSelfSimple(score) + } } else { - baseString = strings.ServiceMessage_GameScoreSelfSimple(score) - } - } else { - if let _ = gameTitle { - baseString = strings.ServiceMessage_GameScoreExtended(score) - } else { - baseString = strings.ServiceMessage_GameScoreSimple(score) - } - } - let baseStringValue = baseString as NSString - var ranges: [(Int, NSRange)] = [] - if baseStringValue.range(of: "{name}").location != NSNotFound { - ranges.append((0, baseStringValue.range(of: "{name}"))) - } - if baseStringValue.range(of: "{game}").location != NSNotFound { - ranges.append((1, baseStringValue.range(of: "{game}"))) - } - ranges.sort(by: { $0.1.location < $1.1.location }) - - var argumentAttributes = peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)]) - argumentAttributes[1] = MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) - attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: argumentAttributes) - case let .paymentSent(currency, totalAmount): - var invoiceMessage: Message? - for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { - invoiceMessage = message - } - } - - var invoiceTitle: String? - if let invoiceMessage = invoiceMessage { - for media in invoiceMessage.media { - if let invoice = media as? TelegramMediaInvoice { - invoiceTitle = invoice.title + if let _ = gameTitle { + baseString = strings.ServiceMessage_GameScoreExtended(score) + } else { + baseString = strings.ServiceMessage_GameScoreSimple(score) } } - } - - if let invoiceTitle = invoiceTitle { - let botString: String - if let peer = messageMainPeer(message) { - botString = peer.compactDisplayTitle + let baseStringValue = baseString as NSString + var ranges: [(Int, NSRange)] = [] + if baseStringValue.range(of: "{name}").location != NSNotFound { + ranges.append((0, baseStringValue.range(of: "{name}"))) + } + if baseStringValue.range(of: "{game}").location != NSNotFound { + ranges.append((1, baseStringValue.range(of: "{game}"))) + } + ranges.sort(by: { $0.1.location < $1.1.location }) + + var argumentAttributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + argumentAttributes[1] = MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [:]) + attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: argumentAttributes) + case let .paymentSent(currency, totalAmount): + var invoiceMessage: Message? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + invoiceMessage = message + } + } + + var invoiceTitle: String? + if let invoiceMessage = invoiceMessage { + for media in invoiceMessage.media { + if let invoice = media as? TelegramMediaInvoice { + invoiceTitle = invoice.title + } + } + } + + if let invoiceTitle = invoiceTitle { + let botString: String + if let peer = messageMainPeer(message) { + botString = peer.compactDisplayTitle + } else { + botString = "" + } + let mutableString = NSMutableAttributedString() + mutableString.append(NSAttributedString(string: strings.Notification_PaymentSent, font: titleFont, textColor: primaryTextColor)) + + var range = NSRange(location: NSNotFound, length: 0) + + range = (mutableString.string as NSString).range(of: "{amount}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor)) + } + range = (mutableString.string as NSString).range(of: "{name}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: botString, font: titleBoldFont, textColor: primaryTextColor)) + } + range = (mutableString.string as NSString).range(of: "{title}") + if range.location != NSNotFound { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: invoiceTitle, font: titleFont, textColor: primaryTextColor)) + } + attributedString = mutableString } else { - botString = "" + attributedString = NSAttributedString(string: strings.Message_PaymentSent(formatCurrencyAmount(totalAmount, currency: currency)).0, font: titleFont, textColor: primaryTextColor) } - let mutableString = NSMutableAttributedString() - mutableString.append(NSAttributedString(string: strings.Notification_PaymentSent, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor)) - - var range = NSRange(location: NSNotFound, length: 0) - - range = (mutableString.string as NSString).range(of: "{amount}") - if range.location != NSNotFound { - mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor)) - } - range = (mutableString.string as NSString).range(of: "{name}") - if range.location != NSNotFound { - mutableString.replaceCharacters(in: range, with: NSAttributedString(string: botString, font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor)) - } - range = (mutableString.string as NSString).range(of: "{title}") - if range.location != NSNotFound { - mutableString.replaceCharacters(in: range, with: NSAttributedString(string: invoiceTitle, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor)) - } - attributedString = mutableString - } else { - attributedString = NSAttributedString(string: strings.Message_PaymentSent(formatCurrencyAmount(totalAmount, currency: currency)).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) - } - case .phoneCall: - break - default: - attributedString = nil + case .phoneCall: + break + default: + attributedString = nil } break } else if let expiredMedia = media as? TelegramMediaExpiredContent { switch expiredMedia.data { case .image: - attributedString = NSAttributedString(string: strings.Message_ImageExpired, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + attributedString = NSAttributedString(string: strings.Message_ImageExpired, font: titleFont, textColor: primaryTextColor) case .file: - attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: primaryTextColor) } } } @@ -400,7 +413,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { let backgroundLayout = self.filledBackgroundNode.asyncLayout() return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - let attributedString = serviceMessageString(theme: item.theme, strings: item.strings, message: item.message, accountPeerId: item.account.peerId) + let attributedString = attributedServiceMessageString(theme: item.theme, strings: item.strings, message: item.message, accountPeerId: item.account.peerId) let (labelLayout, apply) = makeLabelLayout(attributedString, nil, 0, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .center, nil, UIEdgeInsets()) @@ -526,6 +539,17 @@ class ChatMessageActionItemNode: ChatMessageItemView { controllerInteraction.callPeer(peerId) } } + if !foundTapAction { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + foundTapAction = true + break + } + } + } + } if !foundTapAction { self.controllerInteraction?.clickThroughMessage() } @@ -586,7 +610,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { private func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { - var rects: [CGRect]? + var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.frame if let point = point { if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { @@ -599,7 +623,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { - rects = self.labelNode.attributeRects(name: name, at: index) + rects = self.labelNode.lineAndAttributeRects(name: name, at: index) break } } @@ -607,16 +631,19 @@ class ChatMessageActionItemNode: ChatMessageItemView { } if let rects = rects { - var mappedRects = rects - for i in 0 ..< mappedRects.count { - mappedRects[i].origin.x = floor((textNodeFrame.size.width - mappedRects[i].width) / 2.0) + var mappedRects: [CGRect] = [] + for i in 0 ..< rects.count { + let lineRect = rects[i].0 + var itemRect = rects[i].1 + itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x + mappedRects.append(itemRect) } let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) + linkHighlightingNode = LinkHighlightingNode(color: item.theme.chat.serviceMessage.serviceMessageLinkHighlightColor) linkHighlightingNode.inset = 2.5 self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode) diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index 38c31ceaf3..e98f8a83b5 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -349,25 +349,48 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } return (initialWidth, { constrainedSize in - let statusType: ChatMessageDateAndStatusType - if message.effectivelyIncoming { - statusType = .BubbleIncoming - } else { - if message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } + var statusInText = false + var statusSizeAndApply: (CGSize, (Bool) -> Void)? let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - var statusSizeAndApply: (CGSize, (Bool) -> Void)? - - if (refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText { - statusSizeAndApply = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + switch position.bottom { + case .None: + let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText) + statusInText = !imageMode + + let statusType: ChatMessageDateAndStatusType + if message.effectivelyIncoming { + if imageMode { + statusType = .ImageIncoming + } else { + statusType = .BubbleIncoming + } + } else { + if message.flags.contains(.Failed) { + if imageMode { + statusType = .ImageOutgoing(.Failed) + } else { + statusType = .BubbleOutgoing(.Failed) + } + } else if message.flags.isSending { + if imageMode { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sending) + } + } else { + if imageMode { + statusType = .ImageOutgoing(.Sent(read: messageRead)) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + } + + statusSizeAndApply = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + default: + break } let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) @@ -376,7 +399,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var statusFrame: CGRect? - if let (statusSize, _) = statusSizeAndApply { + if statusInText, let (statusSize, _) = statusSizeAndApply { var frame = CGRect(origin: CGPoint(), size: statusSize) let trailingLineWidth = textLayout.trailingLineWidth @@ -489,6 +512,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedBoundingSize.height += imageHeigthAddition + 5.0 adjustedLineHeight += imageHeigthAddition + 4.0 + + if !statusInText, let (statusSize, _) = statusSizeAndApply { + statusFrame = CGRect(origin: CGPoint(), size: statusSize) + } } var contentFileSizeAndApply: (CGSize, () -> ChatMessageInteractiveFileNode)? @@ -513,7 +540,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } var adjustedStatusFrame: CGRect? - if let statusFrame = statusFrame { + if statusInText, let statusFrame = statusFrame { adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size) } @@ -567,10 +594,17 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { contentImageNode.visibility = strongSelf.visibility } let _ = contentImageApply() + let contentImageFrame: CGRect if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { - contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) + contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) } else { - contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) + contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) + } + + contentImageNode.frame = contentImageFrame + + if !statusInText, let statusFrame = statusFrame { + adjustedStatusFrame = CGRect(origin: CGPoint(x: contentImageFrame.width - statusFrame.size.width - 2.0, y: contentImageFrame.height - statusFrame.size.height - 2.0), size: statusFrame.size) } } else if let contentImageNode = strongSelf.contentImageNode { contentImageNode.visibility = .none @@ -629,8 +663,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame { strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) + if statusInText { + if strongSelf.statusNode.supernode != strongSelf { + strongSelf.addSubnode(strongSelf.statusNode) + } + } else if let contentImageNode = strongSelf.contentImageNode { + if strongSelf.statusNode.supernode != contentImageNode { + contentImageNode.addSubnode(strongSelf.statusNode) + } } statusApply(hasAnimation) } else if strongSelf.statusNode.supernode != nil { @@ -670,4 +710,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } return nil } + + func hasActionAtPoint(_ point: CGPoint) -> Bool { + if let buttonNode = self.buttonNode, buttonNode.frame.contains(point) { + return true + } + return false + } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index eacfcffc47..22f451aeb0 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -505,10 +505,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } var needShareButton = false - if let peer = item.message.peers[item.message.id.peerId] { - if let channel = peer as? TelegramChannel { - if case .broadcast = channel.info { - needShareButton = true + if item.message.effectivelyIncoming { + if let peer = item.message.peers[item.message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + needShareButton = true + } + } + } + if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo { + needShareButton = true + } + if !needShareButton { + loop: for media in item.message.media { + if media is TelegramMediaGame || media is TelegramMediaInvoice { + needShareButton = true + break loop + } } } } @@ -1000,7 +1013,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if let selectionNode = self.selectionNode { - if selectionNode.frame.offsetBy(dx: 42.0, dy: 0.0).contains(point) { + var selectionNodeFrame = selectionNode.frame + //selectionNodeFrame.origin.x -= 42.0 + selectionNodeFrame.size.width += 42.0 + if selectionNodeFrame.contains(point) { return selectionNode.view } else { return nil diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index a29e8d8bf2..cb480af38a 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -87,8 +87,6 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { private var flashingOnScrolling = false private var stickDistanceFactor: CGFloat = 0.0 - //private let testNode = ASDisplayNode() - init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { self.timestamp = timestamp self.theme = theme @@ -111,7 +109,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { //self.testNode.backgroundColor = .black //self.testNode.isLayerBacked = true - super.init(layerBacked: true, dynamicBounce: true, isRotated: true, seeThrough: false) + super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) @@ -162,6 +160,12 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { }*/ } + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings @@ -229,4 +233,20 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.labelNode.alpha.isZero { + return nil + } + if self.backgroundNode.frame.contains(point) { + return self.view + } + return nil + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + + } + } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 36cec2582f..e15553f3b6 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -120,7 +120,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { updatedPlaybackStatus = combineLatest(fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in if let pendingStatus = pendingStatus { - return .fetchStatus(.Fetching(progress: pendingStatus.progress)) + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.27) + } + return .fetchStatus(.Fetching(progress: progress)) } else { return resourceStatus } diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index a76cd1d963..a8dfa3fe5d 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -93,10 +93,17 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { + let point = recognizer.location(in: self.imageNode.view) if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { self.activateLocalContent() } else { - self.progressPressed() + if let (_, flags) = self.messageIdAndFlags, flags.isSending { + if let statusNode = self.statusNode, statusNode.frame.contains(point) { + self.progressPressed() + } + } else { + self.progressPressed() + } } } } @@ -174,7 +181,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if isSecretMedia { resultWidth = maxWidth } else { - resultWidth = min(maxWidth, nativeSize.width) + //resultWidth = min(maxWidth, nativeSize.width) + resultWidth = min(constrainedSize.width, nativeSize.aspectFitted(layoutConstants.image.maxDimensions).width) } return (resultWidth, { boundingWidth in @@ -185,8 +193,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { boundingSize = CGSize(width: maxWidth, height: maxWidth) drawingSize = nativeSize.aspectFilled(boundingSize) } else { - drawingSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) - boundingSize = CGSize(width: max(boundingWidth, drawingSize.width), height: drawingSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) + let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) + boundingSize = CGSize(width: boundingWidth, height: fittedSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: layoutConstants.image.maxDimensions.height)) + drawingSize = nativeSize.fitted(boundingSize) } var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -272,7 +281,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(message.id)) |> map { resourceStatus, pendingStatus -> MediaResourceStatus in if let pendingStatus = pendingStatus { - return .Fetching(progress: pendingStatus.progress) + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.027) + } + return .Fetching(progress: progress) } else { return resourceStatus } @@ -284,7 +297,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) |> map { resourceStatus, pendingStatus -> MediaResourceStatus in if let pendingStatus = pendingStatus { - return .Fetching(progress: pendingStatus.progress) + var progress = pendingStatus.progress + if pendingStatus.isRunning { + progress = max(progress, 0.027) + } + return .Fetching(progress: progress) } else { return resourceStatus } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 77bdaa6179..aa18261113 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -50,7 +50,7 @@ struct ChatMessageItemLayoutConstants { self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFillFactor: 0.85, minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 1.0)) self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 260.0, height: 260.0)) + self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) self.instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 2b41f05f45..63c1a779bf 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -19,6 +19,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? + private var highlightedState: Bool = false + required init() { self.imageNode = TransformImageNode() @@ -311,4 +313,25 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } + + override func updateHighlightedState(animated: Bool) { + if let controllerInteraction = self.controllerInteraction, let item = self.item { + var highlighted = false + if let highlightedState = controllerInteraction.highlightedState { + if highlightedState.messageStableId == item.message.stableId { + highlighted = true + } + } + + if self.highlightedState != highlighted { + self.highlightedState = highlighted + + if highlighted { + self.imageNode.setOverlayColor(UIColor(white: 0.0, alpha: 0.2), animated: false) + } else { + self.imageNode.setOverlayColor(nil, animated: animated) + } + } + } + } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 59797355a3..a6630c73bf 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -96,10 +96,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (file, []) } } else if let image = webpage.image { - if let type = webpage.type, ["photo", "video", "embed"].contains(type) { + if let type = webpage.type, ["photo", "video", "embed", "article"].contains(type) { var flags = ChatMessageAttachedContentNodeMediaFlags() - if webpage.instantPage != nil { - flags.insert(.preferMediaBeforeText) + if webpage.instantPage != nil, let largest = largestImageRepresentation(image.representations) { + if largest.dimensions.width >= 256.0 { + flags.insert(.preferMediaBeforeText) + } } mediaAndFlags = (image, flags) } else if let _ = largestImageRepresentation(image.representations)?.dimensions { @@ -167,14 +169,18 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return .instantPage } } - return .ignore + let contentNodeFrame = self.contentNode.frame + if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) { + return .ignore + } + return .none } return .none } override func updateHiddenMedia(_ media: [Media]?) { if let media = media { - var updatedMedia: [Media]? + var updatedMedia = media for item in media { if let webpage = item as? TelegramMediaWebpage, let current = self.webPage, webpage.isEqual(current) { var mediaList: [Media] = [webpage] diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index d3a2e3e82f..bd73105579 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -13,9 +13,6 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let titleNode: TextNode private let textNode: TextNode private let separatorNode: ASDisplayNode - - private let disposable = MetaDisposable() - private var currentMessageId: MessageId? private var currentLayout: CGFloat? private var currentMessage: Message? @@ -81,10 +78,6 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.separatorNode) } - deinit { - self.disposable.dispose() - } - private var theme: PresentationTheme? override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { @@ -98,18 +91,21 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - if self.currentMessageId != interfaceState.pinnedMessageId { - self.currentMessageId = interfaceState.pinnedMessageId - if let pinnedMessageId = interfaceState.pinnedMessageId { - self.disposable.set((singleMessageView(account: account, messageId: pinnedMessageId, loadIfNotExists: true) - |> deliverOnMainQueue).start(next: { [weak self] view in - if let strongSelf = self, let message = view.message { - strongSelf.currentMessage = message - if let currentLayout = strongSelf.currentLayout { - strongSelf.enqueueTransition(width: currentLayout, transition: .immediate, message: message, theme: interfaceState.theme, strings: interfaceState.strings) - } - } - })) + var messageUpdated = false + if let currentMessage = self.currentMessage, let pinnedMessage = interfaceState.pinnedMessage { + if currentMessage.id != pinnedMessage.id || currentMessage.stableVersion != pinnedMessage.stableVersion { + messageUpdated = true + } + } else if (self.currentMessage != nil) != (interfaceState.pinnedMessage != nil) { + messageUpdated = true + } + + if messageUpdated { + let previousMessageWasNil = self.currentMessage == nil + self.currentMessage = interfaceState.pinnedMessage + + if let currentMessage = currentMessage, let currentLayout = self.currentLayout { + self.enqueueTransition(width: currentLayout, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: self.account.peerId, firstTime: previousMessageWasNil) } } @@ -128,35 +124,42 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentLayout = width if let currentMessage = self.currentMessage { - self.enqueueTransition(width: width, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings) + self.enqueueTransition(width: width, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, accountPeerId: interfaceState.accountPeerId, firstTime: true) } } return panelHeight } - private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings) { + private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId, firstTime: Bool) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) - queue.async { [weak self] in + let targetQueue: Queue + if firstTime { + targetQueue = Queue.mainQueue() + } else { + targetQueue = self.queue + } + + targetQueue.async { [weak self] in let leftInset: CGFloat = 10.0 let textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 18.0 let textRightInset: CGFloat = 25.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: descriptionStringForMessage(message, strings: strings, accountPeerId: accountPeerId), font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)) Queue.mainQueue().async { if let strongSelf = self { let _ = titleApply() let _ = textApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 8.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 6.0), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 26.0), size: textLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 24.0), size: textLayout.size) } } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index d11701c648..38654f83e5 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -218,6 +218,7 @@ struct ChatPresentationInterfaceState: Equatable { let titlePanelContexts: [ChatTitlePanelContext] let keyboardButtonsMessage: Message? let pinnedMessageId: MessageId? + let pinnedMessage: Message? let peerIsBlocked: Bool let peerIsMuted: Bool let canReportPeer: Bool @@ -228,8 +229,9 @@ struct ChatPresentationInterfaceState: Equatable { let chatWallpaper: TelegramWallpaper let theme: PresentationTheme let strings: PresentationStrings + let accountPeerId: PeerId - init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { + init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() self.peer = nil @@ -238,6 +240,7 @@ struct ChatPresentationInterfaceState: Equatable { self.titlePanelContexts = [] self.keyboardButtonsMessage = nil self.pinnedMessageId = nil + self.pinnedMessage = nil self.peerIsBlocked = false self.peerIsMuted = false self.canReportPeer = false @@ -248,9 +251,10 @@ struct ChatPresentationInterfaceState: Equatable { self.chatWallpaper = chatWallpaper self.theme = theme self.strings = strings + self.accountPeerId = accountPeerId } - 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) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, accountPeerId: PeerId) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState @@ -259,6 +263,7 @@ struct ChatPresentationInterfaceState: Equatable { self.titlePanelContexts = titlePanelContexts self.keyboardButtonsMessage = keyboardButtonsMessage self.pinnedMessageId = pinnedMessageId + self.pinnedMessage = pinnedMessage self.peerIsBlocked = peerIsBlocked self.peerIsMuted = peerIsMuted self.canReportPeer = canReportPeer @@ -269,6 +274,7 @@ struct ChatPresentationInterfaceState: Equatable { self.chatWallpaper = chatWallpaper self.theme = theme self.strings = strings + self.accountPeerId = accountPeerId } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -314,6 +320,17 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if let lhsMessage = lhs.pinnedMessage, let rhsMessage = rhs.pinnedMessage { + if lhsMessage.id != rhsMessage.id { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + } else if (lhs.pinnedMessage != nil) != (rhs.pinnedMessage != nil) { + return false + } + if lhs.canReportPeer != rhs.canReportPeer { return false } @@ -361,66 +378,74 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.accountPeerId != rhs.accountPeerId { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) + } + + func updatedPinnedMessage(_ pinnedMessage: Message?) -> 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, pinnedMessage: pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } 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, 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) + 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, pinnedMessage: self.pinnedMessage, 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, accountPeerId: self.accountPeerId) } } diff --git a/TelegramUI/ChatTextInputAccessoryItem.swift b/TelegramUI/ChatTextInputAccessoryItem.swift new file mode 100644 index 0000000000..48c1ebd757 --- /dev/null +++ b/TelegramUI/ChatTextInputAccessoryItem.swift @@ -0,0 +1,37 @@ +import Foundation + +enum ChatTextInputAccessoryItem: Equatable { + case keyboard + case stickers + case inputButtons + case messageAutoremoveTimeout(Int32?) + + static func ==(lhs: ChatTextInputAccessoryItem, rhs: ChatTextInputAccessoryItem) -> Bool { + switch lhs { + case .keyboard: + if case .keyboard = rhs { + return true + } else { + return false + } + case .stickers: + if case .stickers = rhs { + return true + } else { + return false + } + case .inputButtons: + if case .inputButtons = rhs { + return true + } else { + return false + } + case let .messageAutoremoveTimeout(lhsTimeout): + if case let .messageAutoremoveTimeout(rhsTimeout) = rhs, lhsTimeout == rhsTimeout { + return true + } else { + return false + } + } + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 8989a76965..e915cd6241 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -17,63 +17,6 @@ private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height context.clear(CGRect(origin: CGPoint(x: (size.width - cutoutWidth) / 2.0, y: 0.0), size: CGSize(width: cutoutWidth, height: size.height / 2.0))) }) -enum ChatTextInputAccessoryItem: Equatable { - case keyboard - case stickers - case inputButtons - case messageAutoremoveTimeout(Int32?) -} - -enum ChatVideoRecordingStatus: Equatable { - case recording(InstantVideoControllerRecordingStatus) - case editing -} - -enum ChatTextInputPanelMediaRecordingState: Equatable { - case audio(recorder: ManagedAudioRecorder, isLocked: Bool) - case video(status: ChatVideoRecordingStatus, isLocked: Bool) - - var isLocked: Bool { - switch self { - case let .audio(_, isLocked): - return isLocked - case let .video(_, isLocked): - return isLocked - } - } - - func withLocked(_ isLocked: Bool) -> ChatTextInputPanelMediaRecordingState { - switch self { - case let .audio(recorder, _): - return .audio(recorder: recorder, isLocked: isLocked) - case let .video(status, _): - return .video(status: status, isLocked: isLocked) - } - } -} - -struct ChatTextInputPanelState: Equatable { - let accessoryItems: [ChatTextInputAccessoryItem] - let contextPlaceholder: NSAttributedString? - let mediaRecordingState: ChatTextInputPanelMediaRecordingState? - - init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, mediaRecordingState: ChatTextInputPanelMediaRecordingState?) { - self.accessoryItems = accessoryItems - self.contextPlaceholder = contextPlaceholder - self.mediaRecordingState = mediaRecordingState - } - - init() { - self.accessoryItems = [] - self.contextPlaceholder = nil - self.mediaRecordingState = nil - } - - func withUpdatedMediaRecordingState(_ mediaRecordingState: ChatTextInputPanelMediaRecordingState?) -> ChatTextInputPanelState { - return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, mediaRecordingState: mediaRecordingState) - } -} - private final class AccessoryItemIconButton: HighlightableButton { private let item: ChatTextInputAccessoryItem @@ -232,10 +175,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - let textFieldInsets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0) - let textInputViewInternalInsets = UIEdgeInsets(top: 6.5, left: 13.0, bottom: 7.5, right: 13.0) - let accessoryButtonSpacing: CGFloat = 0.0 - let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel + private let textFieldInsets = UIEdgeInsets(top: 6.0, left: 42.0, bottom: 6.0, right: 42.0) + private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + private let textInputViewRealInsets = UIEdgeInsets(top: 5.5, left: 0.0, bottom: 6.5, right: 0.0) + private let accessoryButtonSpacing: CGFloat = 0.0 + private let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { self.textInputBackgroundView = UIImageView() @@ -340,6 +284,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.keyboardAppearance = keyboardAppearance + textInputNode.textContainerInset = UIEdgeInsets(top: self.textInputViewRealInsets.top, left: 0.0, bottom: self.textInputViewRealInsets.bottom, right: 0.0) self.addSubnode(textInputNode) self.textInputNode = textInputNode @@ -375,9 +320,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { - textFieldHeight = min(115.0, max(21.0, ceil(textInputNode.measure(CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))) + let unboundTextFieldHeight = max(33.0, ceil(textInputNode.measure(CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) + + textFieldHeight = min(114.0, unboundTextFieldHeight) } else { - textFieldHeight = 21.0 + textFieldHeight = 33.0 } return (accessoryButtonsWidth, textFieldHeight) @@ -695,6 +642,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let textInputNode = self.textInputNode { transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.layout() } if let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder { @@ -713,13 +661,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let _ = placeholderApply() - contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5 + audioRecordingItemsVerticalOffset), size: placeholderSize.size) + contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel + audioRecordingItemsVerticalOffset), size: placeholderSize.size) } else if let contextPlaceholderNode = self.contextPlaceholderNode { self.contextPlaceholderNode = nil contextPlaceholderNode.removeFromSupernode() } - transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5 + audioRecordingItemsVerticalOffset), size: self.textPlaceholderNode.frame.size)) + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel + audioRecordingItemsVerticalOffset), size: self.textPlaceholderNode.frame.size)) transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom)) @@ -976,8 +924,4 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } return super.hitTest(point, with: event) } - - func f2() { - - } } diff --git a/TelegramUI/ChatTextInputPanelNodeOperators.swift b/TelegramUI/ChatTextInputPanelState.swift similarity index 50% rename from TelegramUI/ChatTextInputPanelNodeOperators.swift rename to TelegramUI/ChatTextInputPanelState.swift index d2a4640f6d..8ea3b67761 100644 --- a/TelegramUI/ChatTextInputPanelNodeOperators.swift +++ b/TelegramUI/ChatTextInputPanelState.swift @@ -1,37 +1,46 @@ import Foundation -extension ChatTextInputAccessoryItem { - static func ==(lhs: ChatTextInputAccessoryItem, rhs: ChatTextInputAccessoryItem) -> Bool { - switch lhs { - case .keyboard: - if case .keyboard = rhs { - return true - } else { - return false - } - case .stickers: - if case .stickers = rhs { - return true - } else { - return false - } - case .inputButtons: - if case .inputButtons = rhs { - return true - } else { - return false - } - case let .messageAutoremoveTimeout(lhsTimeout): - if case let .messageAutoremoveTimeout(rhsTimeout) = rhs, lhsTimeout == rhsTimeout { - return true - } else { - return false - } +struct ChatTextInputPanelState: Equatable { + let accessoryItems: [ChatTextInputAccessoryItem] + let contextPlaceholder: NSAttributedString? + let mediaRecordingState: ChatTextInputPanelMediaRecordingState? + + init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, mediaRecordingState: ChatTextInputPanelMediaRecordingState?) { + self.accessoryItems = accessoryItems + self.contextPlaceholder = contextPlaceholder + self.mediaRecordingState = mediaRecordingState + } + + init() { + self.accessoryItems = [] + self.contextPlaceholder = nil + self.mediaRecordingState = nil + } + + func withUpdatedMediaRecordingState(_ mediaRecordingState: ChatTextInputPanelMediaRecordingState?) -> ChatTextInputPanelState { + return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, mediaRecordingState: mediaRecordingState) + } + + static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool { + if lhs.accessoryItems != rhs.accessoryItems { + return false } + if let lhsContextPlaceholder = lhs.contextPlaceholder, let rhsContextPlaceholder = rhs.contextPlaceholder { + return lhsContextPlaceholder.isEqual(to: rhsContextPlaceholder) + } else if (lhs.contextPlaceholder != nil) != (rhs.contextPlaceholder != nil) { + return false + } + if lhs.mediaRecordingState != rhs.mediaRecordingState { + return false + } + return true } } -extension ChatVideoRecordingStatus { +enum ChatVideoRecordingStatus: Equatable { + case recording(InstantVideoControllerRecordingStatus) + case editing + static func ==(lhs: ChatVideoRecordingStatus, rhs: ChatVideoRecordingStatus) -> Bool { switch lhs { case let .recording(lhsStatus): @@ -50,7 +59,28 @@ extension ChatVideoRecordingStatus { } } -extension ChatTextInputPanelMediaRecordingState { +enum ChatTextInputPanelMediaRecordingState: Equatable { + case audio(recorder: ManagedAudioRecorder, isLocked: Bool) + case video(status: ChatVideoRecordingStatus, isLocked: Bool) + + var isLocked: Bool { + switch self { + case let .audio(_, isLocked): + return isLocked + case let .video(_, isLocked): + return isLocked + } + } + + func withLocked(_ isLocked: Bool) -> ChatTextInputPanelMediaRecordingState { + switch self { + case let .audio(recorder, _): + return .audio(recorder: recorder, isLocked: isLocked) + case let .video(status, _): + return .video(status: status, isLocked: isLocked) + } + } + static func ==(lhs: ChatTextInputPanelMediaRecordingState, rhs: ChatTextInputPanelMediaRecordingState) -> Bool { switch lhs { case let .audio(lhsRecorder, lhsIsLocked): @@ -69,20 +99,3 @@ extension ChatTextInputPanelMediaRecordingState { } } -extension ChatTextInputPanelState { - static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool { - if lhs.accessoryItems != rhs.accessoryItems { - return false - } - if let lhsContextPlaceholder = lhs.contextPlaceholder, let rhsContextPlaceholder = rhs.contextPlaceholder { - return lhsContextPlaceholder.isEqual(to: rhsContextPlaceholder) - } else if (lhs.contextPlaceholder != nil) != (rhs.contextPlaceholder != nil) { - return false - } - if lhs.mediaRecordingState != rhs.mediaRecordingState { - return false - } - return true - } -} - diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 2a84bf8b56..479fb4b504 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -6,7 +6,7 @@ import TelegramCore import SwiftSignalKit import LegacyComponents -final class ChatTitleView: UIView { +final class ChatTitleView: UIView, NavigationBarTitleView { private var theme: PresentationTheme private var strings: PresentationStrings @@ -35,9 +35,9 @@ final class ChatTitleView: UIView { if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { switch mergedActivity { case .recordingVoice: - stringValue = "recording audio" + stringValue = strings.Activity_RecordingAudio default: - stringValue = "typing" + stringValue = strings.Conversation_typing } } else { for (peer, _) in inputActivities { @@ -151,14 +151,15 @@ final class ChatTitleView: UIView { } if onlineCount > 1 { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members, ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: "\(onlineCount) online", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.accentTextColor)) + + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true } } else { - let string = NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -166,7 +167,7 @@ final class ChatTitleView: UIView { } } else if let channel = peer as? TelegramChannel { if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - let string = NSAttributedString(string: "\(compactNumericCountString(Int(memberCount))) members", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Conversation_StatusMembers(memberCount), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -174,13 +175,13 @@ final class ChatTitleView: UIView { } else { switch channel.info { case .group: - let string = NSAttributedString(string: "group", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true } case .broadcast: - let string = NSAttributedString(string: "channel", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -297,4 +298,10 @@ final class ChatTitleView: UIView { pressed() } } + + func animateLayoutTransition() { + UIView.transition(with: self, duration: 0.25, options: [.transitionCrossDissolve], animations: { + + }, completion: nil) + } } diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index 42630a0ff3..182abb37ca 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -290,7 +290,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/CommandChatInputContextPanelNode.swift b/TelegramUI/CommandChatInputContextPanelNode.swift index a7dbdcd373..60ac2bda05 100644 --- a/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/TelegramUI/CommandChatInputContextPanelNode.swift @@ -64,7 +64,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = true + self.listView.keepBottomItemOverscrollBackground = .white self.listView.limitHitTestToNodes = true super.init(account: account) diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index f68c0a7bcd..06436fc0c4 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -175,8 +175,8 @@ public class ComposeController: ViewController { private func deactivateSearch() { if !self.displayNavigationBar { - self.contactsNode.deactivateSearch() self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.contactsNode.deactivateSearch() } } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 50b56c4653..bda8570833 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -109,7 +109,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, selection: selection, index: nil, header: header, action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, selection: selection, hasActiveRevealControls: false, index: nil, header: header, action: { _ in interaction.openPeer(peer) }) } diff --git a/TelegramUI/ContactMultiselectionController.swift b/TelegramUI/ContactMultiselectionController.swift index e1641d4798..8d4f5a03fb 100644 --- a/TelegramUI/ContactMultiselectionController.swift +++ b/TelegramUI/ContactMultiselectionController.swift @@ -23,6 +23,8 @@ public class ContactMultiselectionController: ViewController { private let index: PeerNameIndex = .lastNameFirst private var _ready = Promise() + private var _limitsReady = Promise() + private var _listReady = Promise() override public var ready: Promise { return self._ready } @@ -39,6 +41,9 @@ public class ContactMultiselectionController: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var limitsConfiguration: LimitsConfiguration? + private var limitsConfigurationDisposable: Disposable? + public init(account: Account, mode: ContactMultiselectionControllerMode) { self.account = account self.mode = mode @@ -51,22 +56,6 @@ public class ContactMultiselectionController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - switch mode { - case .groupCreation: - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroup, counter: "0/5000") - let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) - self.rightNavigationButton = rightNavigationButton - self.navigationItem.rightBarButtonItem = self.rightNavigationButton - rightNavigationButton.isEnabled = false - case .peerSelection: - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") - let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) - self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) - self.navigationItem.rightBarButtonItem = self.rightNavigationButton - rightNavigationButton.isEnabled = false - } - self.navigationItem.titleView = self.titleView self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -89,6 +78,18 @@ public class ContactMultiselectionController: ViewController { } } }) + + self.limitsConfigurationDisposable = (account.postbox.modify { modifier -> LimitsConfiguration in + return currentLimitsConfiguration(modifier: modifier) + } |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.limitsConfiguration = value + strongSelf.updateTitle() + strongSelf._limitsReady.set(.single(true)) + } + }) + + self._ready.set(combineLatest(self._listReady.get(), self._limitsReady.get()) |> map { $0 && $1 }) } required public init(coder aDecoder: NSCoder) { @@ -97,18 +98,38 @@ public class ContactMultiselectionController: ViewController { deinit { self.presentationDataDisposable?.dispose() + self.limitsConfigurationDisposable?.dispose() } private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) - //self.title = self.presentationData.strings.Contacts_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.updateTitle() + } + + private func updateTitle() { + switch self.mode { + case .groupCreation: + let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroup, counter: "0/\(maxCount)") + let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + self.rightNavigationButton = rightNavigationButton + self.navigationItem.rightBarButtonItem = self.rightNavigationButton + rightNavigationButton.isEnabled = false + case .peerSelection: + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") + let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + self.rightNavigationButton = rightNavigationButton + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) + self.navigationItem.rightBarButtonItem = self.rightNavigationButton + rightNavigationButton.isEnabled = false + } } override public func loadDisplayNode() { self.displayNode = ContactMultiselectionControllerNode(account: self.account) - self._ready.set(self.contactsNode.contactListNode.ready) + self._listReady.set(self.contactsNode.contactListNode.ready) self.contactsNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) @@ -120,14 +141,22 @@ public class ContactMultiselectionController: ViewController { var addedToken: EditableTokenListToken? var removedTokenId: AnyHashable? + let maxRegularCount: Int32 = strongSelf.limitsConfiguration?.maxGroupMemberCount ?? 200 + var displayCountAlert = false + var selectionState: ContactListNodeGroupSelectionState? strongSelf.contactsNode.contactListNode.updateSelectionState { state in if let state = state { - let updatedState = state.withToggledPeerId(peer.id) + var updatedState = state.withToggledPeerId(peer.id) if updatedState.selectedPeerIndices[peer.id] == nil { removedTokenId = peer.id } else { - addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle) + if updatedState.selectedPeerIndices.count >= maxRegularCount { + displayCountAlert = true + updatedState = updatedState.withToggledPeerId(peer.id) + } else { + addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle) + } } updatedCount = updatedState.selectedPeerIndices.count selectionState = updatedState @@ -146,7 +175,8 @@ public class ContactMultiselectionController: ViewController { strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 switch strongSelf.mode { case .groupCreation: - strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/5000") + let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/\(maxCount)") case .peerSelection: break } @@ -160,6 +190,10 @@ public class ContactMultiselectionController: ViewController { } } strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)) + + if displayCountAlert { + strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.CreateGroup_SoftUserLimitAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } } } @@ -192,7 +226,8 @@ public class ContactMultiselectionController: ViewController { strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 switch strongSelf.mode { case .groupCreation: - strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/5000") + let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/\(maxCount)") case .peerSelection: break } diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index 33055c4bbe..e44a856586 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -192,8 +192,8 @@ public class ContactSelectionController: ViewController { private func deactivateSearch() { if !self.displayNavigationBar { - self.contactsNode.deactivateSearch() self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.contactsNode.deactivateSearch() } } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index e12b413266..c09b323f7c 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -135,8 +135,8 @@ public class ContactsController: ViewController { private func deactivateSearch() { if !self.displayNavigationBar { - self.contactsNode.deactivateSearch() self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.contactsNode.deactivateSearch() } } } diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 0720d68d7d..bf64de3cf2 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -68,13 +68,16 @@ final class ContactsControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + insets.top += 20.0 + } + } + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - - if let searchDisplayController = self.searchDisplayController { - searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - } } func activateSearch() { @@ -113,6 +116,7 @@ final class ContactsControllerNode: ASDisplayNode { func deactivateSearch() { if let searchDisplayController = self.searchDisplayController { + self.searchDisplayController = nil var maybePlaceholderNode: SearchBarPlaceholderNode? self.contactListNode.listNode.forEachItemNode { node in if let node = node as? ChatListSearchItemNode { @@ -121,7 +125,6 @@ final class ContactsControllerNode: ASDisplayNode { } searchDisplayController.deactivate(placeholder: maybePlaceholderNode) - self.searchDisplayController = nil } } } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index e579891af9..7297e5da39 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -49,14 +49,18 @@ class ContactsPeerItem: ListViewItem { let chatPeer: Peer? let status: ContactsPeerItemStatus let selection: ContactsPeerItemSelection + let hasActiveRevealControls: Bool let action: (Peer) -> Void + let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? + let deletePeer: ((PeerId) -> Void)? + let selectable: Bool = true let headerAccessoryItem: ListViewAccessoryItem? let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, selection: ContactsPeerItemSelection, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, selection: ContactsPeerItemSelection, hasActiveRevealControls: Bool, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account @@ -64,7 +68,10 @@ class ContactsPeerItem: ListViewItem { self.chatPeer = chatPeer self.status = status self.selection = selection + self.hasActiveRevealControls = hasActiveRevealControls self.action = action + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.deletePeer = deletePeer self.header = header if let index = index { @@ -108,7 +115,12 @@ class ContactsPeerItem: ListViewItem { node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets - completion(node, nodeApply) + completion(node, { + let (signal, apply) = nodeApply() + return (signal, { + apply(false) + }) + }) } } @@ -121,7 +133,7 @@ class ContactsPeerItem: ListViewItem { let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader) Queue.mainQueue().async { completion(nodeLayout, { - apply().1() + apply().1(animation.isAnimated) }) } } @@ -168,13 +180,14 @@ class ContactsPeerItem: ListViewItem { private let separatorHeight = 1.0 / UIScreen.main.scale -class ContactsPeerItemNode: ListViewItemNode { +class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let avatarNode: AvatarNode private let titleNode: TextNode + private var verificationIconNode: ASImageNode? private let statusNode: TextNode private var selectionNode: ASImageNode? @@ -185,6 +198,9 @@ class ContactsPeerItemNode: ListViewItemNode { var peer: Peer? { return self.layoutParams?.0.peer } + private var item: ContactsPeerItem? { + return self.layoutParams?.0 + } required init() { self.backgroundNode = ASDisplayNode() @@ -202,7 +218,7 @@ class ContactsPeerItemNode: ListViewItemNode { self.titleNode = TextNode() self.statusNode = TextNode() - super.init(layerBacked: false, dynamicBounce: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) @@ -256,7 +272,7 @@ class ContactsPeerItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, () -> Void)) { + func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode @@ -290,7 +306,17 @@ class ContactsPeerItemNode: ListViewItemNode { } updatedSelectionImage = selected ? selectedImage : selectableImage } - + + var isVerified = false + if let peer = item.peer as? TelegramUser { + isVerified = peer.flags.contains(.isVerified) + } else if let peer = item.peer as? TelegramChannel { + isVerified = peer.flags.contains(.isVerified) + } + var verificationIconImage: UIImage? + if isVerified { + verificationIconImage = PresentationResourcesChatList.verifiedIcon(item.theme) + } var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? @@ -340,7 +366,12 @@ class ContactsPeerItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + var additionalTitleInset: CGFloat = 0.0 + if let verificationIconImage = verificationIconImage { + additionalTitleInset += 3.0 + verificationIconImage.size.width + } + + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) @@ -359,23 +390,51 @@ class ContactsPeerItemNode: ListViewItemNode { strongSelf.avatarNode.setPeer(account: item.account, peer: peer) } - return (strongSelf.avatarNode.ready, { [weak strongSelf] in + return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in if let strongSelf = strongSelf { strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + let revealOffset = strongSelf.revealOffset + if let _ = updatedTheme { strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: leftInset - 51.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 51.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) let _ = titleApply() - strongSelf.titleNode.frame = titleFrame + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) let _ = statusApply() - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size)) + + if let verificationIconImage = verificationIconImage { + if strongSelf.verificationIconNode == nil { + let verificationIconNode = ASImageNode() + verificationIconNode.isLayerBacked = true + verificationIconNode.displayWithoutProcessing = true + verificationIconNode.displaysAsynchronously = false + strongSelf.verificationIconNode = verificationIconNode + strongSelf.addSubnode(verificationIconNode) + } + if let verificationIconNode = strongSelf.verificationIconNode { + verificationIconNode.image = verificationIconImage + + transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + titleFrame.maxX + 3.0, y: titleFrame.minY + 3.0 + UIScreenPixel), size: verificationIconImage.size)) + } + } else if let verificationIconNode = strongSelf.verificationIconNode { + strongSelf.verificationIconNode = nil + verificationIconNode.removeFromSupernode() + } if let updatedSelectionNode = updatedSelectionNode { if strongSelf.selectionNode !== updatedSelectionNode { @@ -403,15 +462,73 @@ class ContactsPeerItemNode: ListViewItemNode { if let userPresence = userPresence { strongSelf.peerPresenceManager?.reset(presence: userPresence) } + + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: UIColor(rgb: 0xff3824))]) + strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) } }) } else { - return (nil, {}) + return (nil, { _ in + }) } }) } } + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + if let item = self.item { + var leftInset: CGFloat = 65.0 + + switch item.selection { + case .none: + break + case .selectable: + leftInset += 28.0 + } + + var avatarFrame = self.avatarNode.frame + avatarFrame.origin.x = offset + leftInset - 51.0 + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + var titleFrame = self.titleNode.frame + titleFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + var statusFrame = self.statusNode.frame + statusFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.statusNode, frame: statusFrame) + + if let verificationIconNode = self.verificationIconNode { + var iconFrame = verificationIconNode.frame + iconFrame.origin.x = offset + titleFrame.maxX + 3.0 + transition.updateFrame(node: verificationIconNode, frame: iconFrame) + } + } + } + + override func revealOptionsInteractivelyOpened() { + if let item = self.item, let peer = item.peer { + item.setPeerIdWithRevealedOptions?(peer.id, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let item = self.item, let peer = item.peer { + item.setPeerIdWithRevealedOptions?(nil, peer.id) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption) { + if let item = self.item, let peer = item.peer { + item.deletePeer?(peer.id) + } + + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } + override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { let bounds = self.bounds accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 5f97c8bf4c..8bd8fef6bb 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -13,26 +13,33 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openPeer: (PeerId) -> Void + private let dimNode: ASDisplayNode private let listNode: ListView private let searchQuery = Promise() private let searchDisposable = MetaDisposable() + private var presentationData: PresentationData private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> init(account: Account, openPeer: @escaping (PeerId) -> Void) { self.account = account self.openPeer = openPeer - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((presentationData.theme, presentationData.strings)) + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.listNode = ListView() super.init() - self.backgroundColor = presentationData.theme.chatList.backgroundColor + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.dimNode) self.addSubnode(self.listNode) self.listNode.isHidden = true @@ -63,7 +70,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { for item in items { switch item { case let .peer(peer, theme, strings): - listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: nil, action: { [weak self] peer in + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, hasActiveRevealControls: false, index: nil, header: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) @@ -72,29 +79,53 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { } } - strongSelf.listNode.transaction(deleteIndices: (0 ..< previousItems.count).map({ ListViewDeleteItem(index: $0, directionHint: nil) }), insertIndicesAndItems: (0 ..< listItems.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: listItems[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil) + let isEmpty = listItems.isEmpty + + strongSelf.listNode.transaction(deleteIndices: (0 ..< previousItems.count).map({ ListViewDeleteItem(index: $0, directionHint: nil) }), insertIndicesAndItems: (0 ..< listItems.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: listItems[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil, completion: { _ in + if let strongSelf = self { + strongSelf.listNode.isHidden = isEmpty + strongSelf.backgroundColor = isEmpty ? UIColor.black.withAlphaComponent(0.5) : strongSelf.presentationData.theme.chatList.backgroundColor + } + }) } })) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } } deinit { self.searchDisposable.dispose() } + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + override func searchTextUpdated(text: String) { if text.isEmpty { self.searchQuery.set(.single(nil)) - self.listNode.isHidden = true } else { self.searchQuery.set(.single(text)) - self.listNode.isHidden = false } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } } } diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index 501968dabd..b16c62a8ae 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -303,45 +303,45 @@ private struct DataAndStorageData: Equatable { } } -private func stringForUseLessDataSetting(_ settings: VoiceCallSettings) -> String { +private func stringForUseLessDataSetting(strings: PresentationStrings, settings: VoiceCallSettings) -> String { switch settings.dataSaving { case .never: - return "Never" + return strings.CallSettings_Never case .cellular: - return "On Mobile Network" + return strings.CallSettings_OnMobile case .always: - return "Always" + return strings.CallSettings_Always } } private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] - entries.append(.storageUsage(presentationData.theme, "Storage Usage")) - entries.append(.networkUsage(presentationData.theme, "Network Usage")) + entries.append(.storageUsage(presentationData.theme, presentationData.strings.Cache_Title)) + entries.append(.networkUsage(presentationData.theme, presentationData.strings.NetworkUsageSettings_Title)) - entries.append(.automaticPhotoDownloadHeader(presentationData.theme, "AUTOMATIC PHOTO DOWNLOAD")) - entries.append(.automaticPhotoDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.photo.privateChats)) - entries.append(.automaticPhotoDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels)) + entries.append(.automaticPhotoDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticPhotoDownload)) + entries.append(.automaticPhotoDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.photo.privateChats)) + entries.append(.automaticPhotoDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels)) - entries.append(.automaticVoiceDownloadHeader(presentationData.theme, "AUTOMATIC AUDIO DOWNLOAD")) - entries.append(.automaticVoiceDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.voice.privateChats)) - entries.append(.automaticVoiceDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels)) + entries.append(.automaticVoiceDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticAudioDownload)) + entries.append(.automaticVoiceDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.voice.privateChats)) + entries.append(.automaticVoiceDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels)) - entries.append(.automaticInstantVideoDownloadHeader(presentationData.theme, "AUTOMATIC VIDEO MESSAGE DOWNLOAD")) - entries.append(.automaticInstantVideoDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.instantVideo.privateChats)) - entries.append(.automaticInstantVideoDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels)) + entries.append(.automaticInstantVideoDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticVideoMessageDownload)) + entries.append(.automaticInstantVideoDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.instantVideo.privateChats)) + entries.append(.automaticInstantVideoDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels)) /*entries.append(.automaticGifDownloadHeader("AUTOMATIC GIF DOWNLOAD")) entries.append(.automaticGifDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.gif.privateChats)) entries.append(.automaticGifDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.gif.groupsAndChannels))*/ - entries.append(.voiceCallsHeader(presentationData.theme, "VOICE CALLS")) - entries.append(.useLessVoiceData(presentationData.theme, "Use Less Data", stringForUseLessDataSetting(data.voiceCallSettings))) + entries.append(.voiceCallsHeader(presentationData.theme, presentationData.strings.Settings_CallSettings.uppercased())) + entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(strings: presentationData.strings, settings: data.voiceCallSettings))) - entries.append(.otherHeader(presentationData.theme, "OTHER")) - entries.append(.saveIncomingPhotos(presentationData.theme, "Save Incoming Photos", data.automaticMediaDownloadSettings.saveIncomingPhotos)) - entries.append(.saveEditedPhotos(presentationData.theme, "Save Edited Photos", data.generatedMediaStoreSettings.storeEditedPhotos)) + entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) + entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos, data.automaticMediaDownloadSettings.saveIncomingPhotos)) + entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) return entries } @@ -434,7 +434,7 @@ func dataAndStorageController(account: Account) -> ViewController { let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataAndStorageDataPromise.get()) |> deliverOnMainQueue |> map { presentationData, state, dataAndStorageData -> (ItemListControllerState, (ItemListNodeState, DataAndStorageEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Data and Storage"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 5504d79ee8..ed7ea50aaa 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -1,6 +1,7 @@ import Postbox private var telegramUIDeclaredEncodables: Void = { + declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) }) declareEncodable(ChatInterfaceState.self, f: { ChatInterfaceState(decoder: $0) }) declareEncodable(ChatEmbeddedInterfaceState.self, f: { ChatEmbeddedInterfaceState(decoder: $0) }) declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) }) diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index a3c53644cc..878cef3502 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -37,6 +37,7 @@ private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchB inputTextColor: accentColor, inputPlaceholderTextColor: UIColor(rgb: 0x5e5e5e), inputIconColor: UIColor(rgb: 0x5e5e5e), + inputClearButtonColor: UIColor(rgb: 0x5e5e5e), separatorColor: UIColor(rgb: 0x1a1a1a) ) @@ -87,10 +88,12 @@ private let chatList = PresentationThemeChatList( messageDraftTextColor: UIColor(rgb: 0xdd4b39), checkmarkColor: UIColor(rgb: 0x545454), pendingIndicatorColor: UIColor(rgb: 0x545454), + muteIconColor: UIColor(rgb: 0x626262), unreadBadgeActiveBackgroundColor: UIColor(rgb: 0xb2b2b2), unreadBadgeActiveTextColor: UIColor(rgb: 0x121212), unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0x626262), unreadBadgeInactiveTextColor:UIColor(rgb: 0x121212), + pinnedBadgeColor: UIColor(rgb: 0x121212), pinnedSearchBarColor: UIColor(rgb: 0x545454), regularSearchBarColor: UIColor(rgb: 0x545454), sectionHeaderFillColor: UIColor(rgb: 0x000000), @@ -144,6 +147,7 @@ private let bubble = PresentationThemeChatBubble( private let serviceMessage = PresentationThemeServiceMessage( serviceMessageFillColor: UIColor(rgb: 0xffffff, alpha: 0.2), serviceMessagePrimaryTextColor: UIColor(rgb: 0xb2b2b2), + serviceMessageLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.2), unreadBarFillColor: UIColor(rgb: 0x1b1b1b), unreadBarStrokeColor: UIColor(rgb: 0x000000), unreadBarTextColor: UIColor(rgb: 0xb2b2b2), diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index a0ca9d0413..819a8d5673 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -38,6 +38,7 @@ private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchB inputTextColor: .black, inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), inputIconColor: UIColor(rgb: 0x8e8e93), + inputClearButtonColor: UIColor(rgb: 0x7b7b81), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) ) @@ -88,11 +89,13 @@ private let chatList = PresentationThemeChatList( messageDraftTextColor: UIColor(rgb: 0xdd4b39), checkmarkColor: UIColor(rgb: 0x21c004), pendingIndicatorColor: UIColor(rgb: 0x8e8e93), - unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5), + muteIconColor: UIColor(rgb: 0xa7a7ad), + unreadBadgeActiveBackgroundColor: accentColor, unreadBadgeActiveTextColor: .white, - unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xadb3bb), + unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb), unreadBadgeInactiveTextColor: .white, - pinnedSearchBarColor: UIColor(rgb: 0xdfdfdf), + pinnedBadgeColor: UIColor(rgb: 0x939399), + pinnedSearchBarColor: UIColor(rgb: 0xe5e5e5), regularSearchBarColor: UIColor(rgb: 0xe9e9e9), sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), @@ -145,6 +148,7 @@ private let bubble = PresentationThemeChatBubble( private let serviceMessage = PresentationThemeServiceMessage( serviceMessageFillColor: UIColor(rgb: 0x748391, alpha: 0.45), serviceMessagePrimaryTextColor: .white, + serviceMessageLinkHighlightColor: UIColor(rgb: 0x748391, alpha: 0.25), unreadBarFillColor: UIColor(white: 1.0, alpha: 0.9), unreadBarStrokeColor: UIColor(white: 0.0, alpha: 0.2), unreadBarTextColor: UIColor(rgb: 0x86868d), @@ -181,7 +185,7 @@ private let inputMediaPanel = PresentationThemeInputMediaPanel( private let inputButtonPanel = PresentationThemeInputButtonPanel( panelSerapatorColor: UIColor(rgb: 0xBEC2C6), - panelBackgroundColor: UIColor(rgb: 0x9099A2), + panelBackgroundColor: UIColor(rgb: 0xdee2e6), buttonFillColor: .white, buttonStrokeColor: UIColor(rgb: 0xc3c7c9), buttonHighlightedFillColor: UIColor(rgb: 0xa8b3c0), diff --git a/TelegramUI/EmbedGalleryVideoItem.swift b/TelegramUI/EmbedGalleryVideoItem.swift index 7da87c266a..6295db70a5 100644 --- a/TelegramUI/EmbedGalleryVideoItem.swift +++ b/TelegramUI/EmbedGalleryVideoItem.swift @@ -241,7 +241,7 @@ final class EmbedVideoGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/FetchManager.swift b/TelegramUI/FetchManager.swift new file mode 100644 index 0000000000..a72e13e1c0 --- /dev/null +++ b/TelegramUI/FetchManager.swift @@ -0,0 +1,159 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +private struct FetchManagerLocationEntryId: Hashable { + let resourceId: MediaResourceId + let locationKey: FetchManagerLocationKey + + static func ==(lhs: FetchManagerLocationEntryId, rhs: FetchManagerLocationEntryId) -> Bool { + if !lhs.resourceId.isEqual(to: rhs.resourceId) { + return false + } + if !lhs.locationKey.isEqual(to: rhs.locationKey) { + return false + } + return true + } + + var hashValue: Int { + return self.resourceId.hashValue &* 31 &+ self.locationKey.hashValue + } +} + +private final class FetchManagerLocationEntry { + let id: FetchManagerLocationEntryId + let resource: MediaResource + + var referenceCount: Int32 = 0 + var elevatedPriorityReferenceCount: Int32 = 0 + var userInitiatedPriorityIndices: [Int32] = [] + + var priorityKey: FetchManagerPriorityKey? { + if self.referenceCount > 0 { + return FetchManagerPriorityKey(locationKey: self.id.locationKey, hasElevatedPriority: self.elevatedPriorityReferenceCount > 0, userInitiatedPriority: userInitiatedPriorityIndices.last) + } else { + return nil + } + } + + init(id: FetchManagerLocationEntryId, resource: MediaResource) { + self.id = id + self.resource = resource + } +} + +private final class FetchManagerCategoryLocationContext { + private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)? + private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:] + + func withEntry(id: FetchManagerLocationEntryId, resource: MediaResource, _ f: (FetchManagerLocationEntry) -> Void) { + let entry: FetchManagerLocationEntry + let previousPriorityKey: FetchManagerPriorityKey? + if let current = self.entries[id] { + entry = current + previousPriorityKey = entry.priorityKey + } else { + previousPriorityKey = nil + entry = FetchManagerLocationEntry(id: id, resource: resource) + self.entries[id] = entry + } + + f(entry) + + let updatedPriorityKey = entry.priorityKey + if previousPriorityKey != updatedPriorityKey { + if let updatedPriorityKey = updatedPriorityKey { + if let (topId, topPriority) = self.topEntryIdAndPriority { + if updatedPriorityKey < topPriority { + self.topEntryIdAndPriority = (entry.id, updatedPriorityKey) + } else if updatedPriorityKey > topPriority && topId == id { + self.topEntryIdAndPriority = nil + } + } else { + self.topEntryIdAndPriority = (entry.id, updatedPriorityKey) + } + } else { + if self.topEntryIdAndPriority?.0 == id { + self.topEntryIdAndPriority = nil + } + self.entries.removeValue(forKey: id) + } + } + + if self.topEntryIdAndPriority == nil && !self.entries.isEmpty { + var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)? + for (id, entry) in self.entries { + if let entryPriorityKey = entry.priorityKey { + if let (_, topKey) = topEntryIdAndPriority { + if entryPriorityKey < topKey { + topEntryIdAndPriority = (id, entryPriorityKey) + } + } else { + topEntryIdAndPriority = (id, entryPriorityKey) + } + } else { + assertionFailure() + } + } + + self.topEntryIdAndPriority = topEntryIdAndPriority + } + } + + var isEmpty: Bool { + return self.entries.isEmpty + } +} + +final class FetchManager { + private let queue = Queue() + private let network: Network + + private var categoryLocationContexts: [FetchManagerCategoryLocationKey: FetchManagerCategoryLocationContext] = [:] + + init(network: Network) { + self.network = network + } + + private func withLocationContext(_ key: FetchManagerCategoryLocationKey, _ f: (FetchManagerCategoryLocationContext) -> Void) { + assert(self.queue.isCurrent()) + let context: FetchManagerCategoryLocationContext + if let current = self.categoryLocationContexts[key] { + context = current + } else { + context = FetchManagerCategoryLocationContext() + self.categoryLocationContexts[key] = context + } + + f(context) + + if context.isEmpty { + self.categoryLocationContexts.removeValue(forKey: key) + } + } + + func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource, elevatedPriority: Bool, userInitiated: Bool) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + strongSelf.withLocationContext(FetchManagerCategoryLocationKey(location: location, category: category), { context in + context.withEntry(id: FetchManagerLocationEntryId(resourceId: resource.id, locationKey: locationKey), resource: resource, { entry in + + }) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(self.queue) + } +} diff --git a/TelegramUI/FetchManagerLocation.swift b/TelegramUI/FetchManagerLocation.swift new file mode 100644 index 0000000000..f7bc4f175d --- /dev/null +++ b/TelegramUI/FetchManagerLocation.swift @@ -0,0 +1,101 @@ +import Foundation +import Postbox + +enum FetchManagerCategory: Int32 { + case image + case file +} + +protocol FetchManagerLocationKey: class { + func isEqual(to: FetchManagerLocationKey) -> Bool + func isLess(than: FetchManagerLocationKey) -> Bool + var hashValue: Int { get } +} + +struct FetchManagerCategoryLocationKey: Hashable { + let location: FetchManagerLocation + let category: FetchManagerCategory + + var hashValue: Int { + return self.location.hashValue &* 31 &+ self.category.hashValue + } + + static func ==(lhs: FetchManagerCategoryLocationKey, rhs: FetchManagerCategoryLocationKey) -> Bool { + if lhs.location != rhs.location { + return false + } + if lhs.category != rhs.category { + return false + } + return true + } +} + +struct FetchManagerPriorityKey: Comparable { + let locationKey: FetchManagerLocationKey + let hasElevatedPriority: Bool + let userInitiatedPriority: Int32? + + static func ==(lhs: FetchManagerPriorityKey, rhs: FetchManagerPriorityKey) -> Bool { + if !lhs.locationKey.isEqual(to: rhs.locationKey) { + return false + } + if lhs.hasElevatedPriority != rhs.hasElevatedPriority { + return false + } + if lhs.userInitiatedPriority != rhs.userInitiatedPriority { + return false + } + return true + } + + static func <(lhs: FetchManagerPriorityKey, rhs: FetchManagerPriorityKey) -> Bool { + if let lhsUserInitiatedPriority = lhs.userInitiatedPriority, let rhsUserInitiatedPriority = rhs.userInitiatedPriority { + if lhsUserInitiatedPriority != rhsUserInitiatedPriority { + if lhsUserInitiatedPriority < rhsUserInitiatedPriority { + return false + } else { + return true + } + } + } else if (lhs.userInitiatedPriority != nil) != (rhs.userInitiatedPriority != nil) { + if lhs.userInitiatedPriority != nil { + return false + } else { + return true + } + } + + if lhs.hasElevatedPriority != rhs.hasElevatedPriority { + if lhs.hasElevatedPriority { + return false + } else { + return true + } + } + + return lhs.locationKey.isLess(than: rhs.locationKey) + } +} + +enum FetchManagerLocation: Hashable { + case chat(PeerId) + + static func ==(lhs: FetchManagerLocation, rhs: FetchManagerLocation) -> Bool { + switch lhs { + case let .chat(peerId): + if case .chat(peerId) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .chat(peerId): + return peerId.hashValue + } + } +} diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 2937dcbde7..7a023c6f60 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -80,7 +80,6 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr } else if let file = media as? TelegramMediaFile { if file.isVideo || file.mimeType.hasPrefix("video/") { return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(file: file), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, caption: message.text) - //return ChatVideoGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else { if file.mimeType.hasPrefix("image/") { return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) @@ -88,8 +87,10 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr return ChatDocumentGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } } - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - return EmbedVideoGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = webpage.content { + if let content = WebEmbedVideoContent(webpageContent: webpageContent) { + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, caption: message.text) + } } } default: @@ -169,7 +170,7 @@ class GalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void private let baseNavigationController: NavigationController? - init(account: Account, messageId: MessageId, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { + init(account: Account, messageId: MessageId, invertItemOrder: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { self.account = account self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController @@ -188,7 +189,7 @@ class GalleryController: ViewController { |> filter({ $0 != nil }) |> mapToSignal { message -> Signal in if let tags = tagsForMessage(message!) { - let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) + let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, clipHoles: false, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) return view |> mapToSignal { (view, _, _) -> Signal in diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index cf90539925..d93016fc8c 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -4,6 +4,37 @@ import Display import SwiftSignalKit import Postbox +struct GalleryPagerInsertItem { + public let index: Int + public let item: GalleryItem + public let previousIndex: Int? + + public init(index: Int, item: GalleryItem, previousIndex: Int?) { + self.index = index + self.item = item + self.previousIndex = previousIndex + } +} + +struct GalleryPagerUpdateItem { + let index: Int + let previousIndex: Int + let item: GalleryItem + + init(index: Int, previousIndex: Int, item: GalleryItem) { + self.index = index + self.previousIndex = previousIndex + self.item = item + } +} + +struct GalleryPagerTransaction { + let deleteItems: [Int] + let insertItems: [GalleryPagerInsertItem] + let updateItems: [GalleryPagerUpdateItem] + let focusOnItem: Int? +} + final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private let pageGap: CGFloat @@ -87,26 +118,85 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, keepFirst: Bool = false) { - var keptItemNode: GalleryItemNode? - for itemNode in self.itemNodes { - if keepFirst && itemNode.index == 0 { - keptItemNode = itemNode + var updateItems: [GalleryPagerUpdateItem] = [] + let deleteItems: [Int] = [] + var insertItems: [GalleryPagerInsertItem] = [] + for i in 0 ..< items.count { + if i == 0 && keepFirst { + updateItems.append(GalleryPagerUpdateItem(index: 0, previousIndex: 0, item: items[i])) } else { - itemNode.removeFromSupernode() + insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: nil)) } } - self.itemNodes.removeAll() - if let keptItemNode = keptItemNode { - self.itemNodes.append(keptItemNode) + self.transaction(GalleryPagerTransaction(deleteItems: deleteItems, insertItems: insertItems, updateItems: updateItems, focusOnItem: centralItemIndex)) + } + + func transaction(_ transaction: GalleryPagerTransaction) { + for updatedItem in transaction.updateItems { + self.items[updatedItem.previousIndex] = updatedItem.item + if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) { + updatedItem.item.updateNode(node: itemNode) + } } - if let centralItemIndex = centralItemIndex, centralItemIndex >= 0 && centralItemIndex < items.count { - self.centralItemIndex = centralItemIndex - } else { - self.centralItemIndex = nil - } - self.items = items - self.updateItemNodes() + var removedNodes: [GalleryItemNode] = [] + + if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty { + let deleteItems = transaction.deleteItems.sorted() + + for deleteItemIndex in deleteItems.reversed() { + self.items.remove(at: deleteItemIndex) + for i in 0 ..< self.itemNodes.count { + if self.itemNodes[i].index == deleteItemIndex { + removedNodes.append(self.itemNodes[i]) + self.removeVisibleItemNode(internalIndex: i) + } + } + } + + for itemNode in self.itemNodes { + var indexOffset = 0 + for deleteIndex in deleteItems { + if deleteIndex < itemNode.index { + indexOffset += 1 + } else { + break + } + } + + itemNode.index = itemNode.index - indexOffset + } + + let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + if self.items.count == 0 && !insertItems.isEmpty { + if insertItems[0].index != 0 { + fatalError("transaction: invalid insert into empty list") + } + } + + for insertedItem in insertItems { + self.items.insert(insertedItem.item, at: insertedItem.index) + } + + let sortedInsertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + + for itemNode in self.itemNodes { + var indexOffset = 0 + for insertedItem in sortedInsertItems { + if insertedItem.index <= itemNode.index + indexOffset { + indexOffset += 1 + } + } + + itemNode.index = itemNode.index + indexOffset + } + + if let focusOnItem = transaction.focusOnItem { + self.centralItemIndex = focusOnItem + } + + self.updateItemNodes() + } } private func makeNodeForItem(at index: Int) -> GalleryItemNode { @@ -282,3 +372,4 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } } } + diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index a1e246da24..1736381190 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -15,6 +15,7 @@ private final class GroupInfoArguments { let pushController: (ViewController) -> Void let presentController: (ViewController, ViewControllerPresentationArguments) -> Void let changeNotificationMuteSettings: () -> Void + let changeNotificationSoundSettings: () -> Void let openSharedMedia: () -> Void let openAdminManagement: () -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void @@ -23,8 +24,9 @@ private final class GroupInfoArguments { let addMember: () -> Void let removePeer: (PeerId) -> Void let convertToSupergroup: () -> Void + let leave: () -> Void - init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void) { + init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void) { self.account = account self.peerId = peerId self.avatarAndNameInfoContext = avatarAndNameInfoContext @@ -33,6 +35,7 @@ private final class GroupInfoArguments { self.pushController = pushController self.presentController = presentController self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.changeNotificationSoundSettings = changeNotificationSoundSettings self.openSharedMedia = openSharedMedia self.openAdminManagement = openAdminManagement self.updateEditingName = updateEditingName @@ -41,14 +44,15 @@ private final class GroupInfoArguments { self.addMember = addMember self.removePeer = removePeer self.convertToSupergroup = convertToSupergroup + self.leave = leave } } private enum GroupInfoSection: ItemListSectionId { case info case about - case sharedMediaAndNotifications case infoManagement + case sharedMediaAndNotifications case memberManagement case members case leave @@ -97,6 +101,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case link(PresentationTheme, String) case sharedMedia(PresentationTheme) case notifications(PresentationTheme, settings: PeerNotificationSettings?) + case notificationSound(PresentationTheme, String, String) case adminManagement(PresentationTheme) case groupTypeSetup(PresentationTheme, isPublic: Bool) case groupDescriptionSetup(PresentationTheme, text: String) @@ -114,10 +119,10 @@ private enum GroupInfoEntry: ItemListNodeEntry { return GroupInfoSection.info.rawValue case .about, .link: return GroupInfoSection.about.rawValue - case .sharedMedia, .notifications, .adminManagement: - return GroupInfoSection.sharedMediaAndNotifications.rawValue case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel: return GroupInfoSection.infoManagement.rawValue + case .sharedMedia, .notifications, .notificationSound, .adminManagement: + return GroupInfoSection.sharedMediaAndNotifications.rawValue case .membersAdmins, .membersBlacklist: return GroupInfoSection.memberManagement.rawValue case .addMember, .member: @@ -217,6 +222,12 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } + case let .notificationSound(lhsTheme, lhsTitle, lhsValue): + if case let .notificationSound(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { + return true + } else { + return false + } case let .groupTypeSetup(lhsTheme, lhsIsPublic): if case let .groupTypeSetup(rhsTheme, rhsIsPublic) = rhs, lhsTheme == rhsTheme, lhsIsPublic == rhsIsPublic { return true @@ -309,24 +320,26 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 2 case .link: return 3 - case .notifications: - return 4 - case .sharedMedia: - return 5 case .adminManagement: - return 6 + return 4 case .groupTypeSetup: - return 7 + return 5 case .groupDescriptionSetup: + return 6 + case .notifications: + return 7 + case .notificationSound: return 8 - case .groupManagementInfoLabel: + case .sharedMedia: return 9 - case .membersAdmins: + case .groupManagementInfoLabel: return 10 - case .membersBlacklist: + case .membersAdmins: return 11 - case .addMember: + case .membersBlacklist: return 12 + case .addMember: + return 13 case let .member(_, index, _, _, _, _, _, _): return 20 + index case .convertToSupergroup: @@ -367,6 +380,10 @@ private enum GroupInfoEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationMuteSettings() }) + case let .notificationSound(theme, title, value): + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + arguments.changeNotificationSoundSettings() + }) case let .sharedMedia(theme): return ItemListDisclosureItem(theme: theme, title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { arguments.openSharedMedia() @@ -420,6 +437,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { }) case let .leave(theme): return ItemListActionItem(theme: theme, title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + arguments.leave() }) default: preconditionFailure() @@ -551,12 +569,8 @@ private func canRemoveParticipant(account: Account, isAdmin: Bool, participantId return isAdmin } -private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: GroupInfoState) -> [GroupInfoEntry] { +private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, globalNotificationSettings: GlobalNotificationSettings, state: GroupInfoState) -> [GroupInfoEntry] { var entries: [GroupInfoEntry] = [] - if let peer = peerViewMainPeer(view) { - let infoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName) - entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) - } var highlightAdmins = false var canEditGroupInfo = false @@ -603,10 +617,17 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa } } + if let peer = peerViewMainPeer(view) { + let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditGroupInfo ? nil : state.editingState?.editingName, updatingName: state.updatingName) + entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) + } + if canEditGroupInfo { entries.append(GroupInfoEntry.setGroupPhoto(presentationData.theme)) } + let peerNotificationSettings: TelegramPeerNotificationSettings = (view.notificationSettings as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + if let editingState = state.editingState { if let group = view.peers[view.peerId] as? TelegramGroup, case .creator = group.role { entries.append(.adminManagement(presentationData.theme)) @@ -618,6 +639,9 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa entries.append(GroupInfoEntry.groupDescriptionSetup(presentationData.theme, text: editingState.editingDescriptionText)) } + entries.append(GroupInfoEntry.notifications(presentationData.theme, settings: view.notificationSettings)) + entries.append(GroupInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: peerNotificationSettings.messageSound, default: globalNotificationSettings.effective.groupChats.sound))) + if let adminCount = cachedChannelData.participantsSummary.adminCount { entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, count: Int(adminCount))) } @@ -882,6 +906,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var popToRootImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -1019,6 +1044,17 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, changeNotificationSoundSettings: { + let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } |> deliverOnMainQueue).start(next: { settings in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.groupChats.sound, completion: { sound in + let _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start() + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) }, openSharedMedia: { if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) @@ -1198,11 +1234,50 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl removeMemberDisposable.set(signal.start()) }, convertToSupergroup: { pushControllerImpl?(convertToSupergroupController(account: account, peerId: peerId)) + }, leave: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let notificationAction: (Int32) -> Void = { muteUntil in + let muteState: PeerMuteState + if muteUntil <= 0 { + muteState = .unmuted + } else if muteUntil == Int32.max { + muteState = .muted(until: Int32.max) + } else { + muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + } + changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.DialogList_DeleteConversationConfirmation, color: .destructive, action: { + dismissAction() + let _ = (removePeerChat(postbox: account.postbox, peerId: peerId, reportChatSpam: false) + |> deliverOnMainQueue).start(completed: { + popToRootImpl?() + }) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId)) - |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [globalNotificationsKey])) + |> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) + + var globalNotificationSettings: GlobalNotificationSettings = GlobalNotificationSettings.defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + let rightNavigationButton: ItemListNavigationButton if let editingState = state.editingState { var doneEnabled = true @@ -1277,7 +1352,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .blocks) + let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -1292,6 +1367,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + popToRootImpl = { [weak controller] in + (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) + } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: (ASDisplayNode, CGRect)? diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift index 2c4b7a7bfe..fda290eec7 100644 --- a/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -4,12 +4,24 @@ import Postbox import TelegramCore import Display +private struct HashtagChatInputContextPanelEntryStableId: Hashable { + let text: String + + var hashValue: Int { + return self.text.hashValue + } + + static func ==(lhs: HashtagChatInputContextPanelEntryStableId, rhs: HashtagChatInputContextPanelEntryStableId) -> Bool { + return lhs.text == rhs.text + } +} + private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let text: String - var stableId: Int { - return self.text.hashValue + var stableId: HashtagChatInputContextPanelEntryStableId { + return HashtagChatInputContextPanelEntryStableId(text: self.text) } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { @@ -20,7 +32,7 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(hashtagSelected: @escaping (String) -> Void) -> ListViewItem { + func item(account: Account, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { return HashtagChatInputPanelItem(text: self.text, hashtagSelected: hashtagSelected) } } @@ -31,12 +43,12 @@ private struct HashtagChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], hashtagSelected: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, hashtagSelected: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(hashtagSelected: hashtagSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(hashtagSelected: hashtagSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -52,7 +64,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.stackFromBottomInsetItemFactor = 3.5 + self.listView.keepBottomItemOverscrollBackground = .white self.listView.limitHitTestToNodes = true super.init(account: account) @@ -66,19 +78,19 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { func updateResults(_ results: [String]) { var entries: [HashtagChatInputContextPanelEntry] = [] var index = 0 - var textSet = Set() + var stableIds = Set() for text in results { - let textHash = text.hashValue - if textSet.contains(textHash) { + let entry = HashtagChatInputContextPanelEntry(index: index, text: text) + if stableIds.contains(entry.stableId) { continue } - textSet.insert(textHash) - entries.append(HashtagChatInputContextPanelEntry(index: index, text: text)) + stableIds.insert(entry.stableId) + entries.append(entry) index += 1 } let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hashtagSelected: { [weak self] text in + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in if let (range, type, _) = textInputStateContextQueryRangeAndType(textInputState) { @@ -122,12 +134,18 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { var options = ListViewDeleteAndInsertOptions() if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) + //options.insert(.Synchronous) + //options.insert(.LowLatency) } else { - //options.insert(.AnimateInsertion) + options.insert(.AnimateTopItemPosition) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(size: self.listView.bounds.size) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default) + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self, firstTime { var topItemOffset: CGFloat? strongSelf.listView.forEachItemNode { itemNode in @@ -145,24 +163,31 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } } + private func topInsetForLayout(size: CGSize) -> CGFloat { + var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + + return max(size.height - minimumItemHeights, 0.0) + } + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { - let insets = UIEdgeInsets() + var insets = UIEdgeInsets() + insets.top = self.topInsetForLayout(size: size) transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) var duration: Double = 0.0 var curve: UInt = 0 switch transition { - case .immediate: + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 - } + case .spring: + curve = 7 + } } let listViewCurve: ListViewAnimationCurve @@ -207,3 +232,4 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } } + diff --git a/TelegramUI/HashtagChatInputPanelItem.swift b/TelegramUI/HashtagChatInputPanelItem.swift index 6520fe5b93..d5de1de1fd 100644 --- a/TelegramUI/HashtagChatInputPanelItem.swift +++ b/TelegramUI/HashtagChatInputPanelItem.swift @@ -115,13 +115,13 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { let leftInset: CGFloat = 15.0 let rightInset: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: "#\(item.text)", font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { - textApply() + let _ = textApply() strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index d5a34cd75a..9a3959fa96 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -11,75 +11,96 @@ final class HorizontalPeerItem: ListViewItem { let account: Account let peer: Peer let action: (Peer) -> Void + let isPeerSelected: (PeerId) -> Bool + let customWidth: CGFloat? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, action: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, action: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { self.theme = theme self.strings = strings self.account = account self.peer = peer self.action = action + self.isPeerSelected = isPeerSelected + self.customWidth = customWidth } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = HorizontalPeerItemNode() - node.contentSize = CGSize(width: 92.0, height: 80.0) - node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) - node.update(account: self.account, peer: self.peer, theme: self.theme, strings: self.strings) - node.action = self.action + + let (nodeLayout, apply) = node.asyncLayout()(self, width) + + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + completion(node, { - return (nil, {}) + return (nil, { + apply(false) + }) }) } } 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), { - }) + assert(node is HorizontalPeerItemNode) + if let node = node as? HorizontalPeerItemNode { + Queue.mainQueue().async { + let layout = node.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, width) + Queue.mainQueue().async { + completion(nodeLayout, { + apply(animation.isAnimated) + }) + } + } + } + } } } final class HorizontalPeerItemNode: ListViewItemNode { - private let avatarNode: AvatarNode - private let titleNode: ASTextNode - private(set) var peer: Peer? - fileprivate var action: ((Peer) -> Void)? + private(set) var peerNode: SelectablePeerNode + + private(set) var item: HorizontalPeerItem? init() { - self.avatarNode = AvatarNode(font: Font.regular(14.0)) - //self.avatarNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) - self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) - - self.titleNode = ASTextNode() - //self.titleNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.peerNode = SelectablePeerNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.avatarNode) - self.addSubnode(self.titleNode) + self.addSubnode(self.peerNode) + self.peerNode.toggleSelection = { [weak self] in + if let item = self?.item { + item.action(item.peer) + } + } } override func didLoad() { super.didLoad() self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func update(account: Account, peer: Peer, theme: PresentationTheme, strings: PresentationStrings) { - self.peer = peer - self.avatarNode.setPeer(account: account, peer: peer) - self.titleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.regular(11.0), textColor: theme.list.itemPrimaryTextColor) - let titleSize = self.titleNode.measure(CGSize(width: 84.0, height: CGFloat.infinity)) - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - titleSize.width) / 2.0), y: 4.0 + 60.0 + 6.0), size: titleSize) + func asyncLayout() -> (HorizontalPeerItem, CGFloat) -> (ListViewItemNodeLayout, (Bool) -> Void) { + return { [weak self] item, width in + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 92.0, height: item.customWidth ?? 80.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.peerNode.setup(account: item.account, peer: item.peer, chatPeer: nil, numberOfLines: 1) + strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) + strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) + } + }) + } } - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let peer = self.peer, let action = self.action { - action(peer) - } + func updateSelection(animated: Bool) { + if let item = self.item { + self.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: animated) } } } diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 623853ff83..9497bf2433 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -2,12 +2,12 @@ import Foundation import Postbox import SwiftSignalKit -struct InAppNotificationSettings: PreferencesEntry, Equatable { - let playSounds: Bool - let vibrate: Bool - let displayPreviews: Bool +public struct InAppNotificationSettings: PreferencesEntry, Equatable { + public let playSounds: Bool + public let vibrate: Bool + public let displayPreviews: Bool - static var defaultSettings: InAppNotificationSettings { + public static var defaultSettings: InAppNotificationSettings { return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true) } @@ -17,13 +17,13 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { self.displayPreviews = displayPreviews } - init(decoder: PostboxDecoder) { + public init(decoder: PostboxDecoder) { self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0 self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0 self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 } - func encode(_ encoder: PostboxEncoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.playSounds ? 1 : 0, forKey: "s") encoder.encodeInt32(self.vibrate ? 1 : 0, forKey: "v") encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p") @@ -41,7 +41,7 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { return InAppNotificationSettings(playSounds: self.playSounds, vibrate: self.vibrate, displayPreviews: displayPreviews) } - func isEqual(to: PreferencesEntry) -> Bool { + public func isEqual(to: PreferencesEntry) -> Bool { if let to = to as? InAppNotificationSettings { return self == to } else { @@ -49,7 +49,7 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { } } - static func ==(lhs: InAppNotificationSettings, rhs: InAppNotificationSettings) -> Bool { + public static func ==(lhs: InAppNotificationSettings, rhs: InAppNotificationSettings) -> Bool { if lhs.playSounds != rhs.playSounds { return false } diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index 23f879516a..a8492afe34 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -126,7 +126,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -156,7 +156,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index 0c8cba5c4b..cbbb4dc3ec 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -7,6 +7,7 @@ import Display final class InstantPageController: ViewController { private let account: Account private var webPage: TelegramMediaWebpage + private let anchor: String? private var presentationData: PresentationData @@ -24,11 +25,12 @@ final class InstantPageController: ViewController { private var settings: InstantPagePresentationSettings? private var settingsDisposable: Disposable? - init(account: Account, webPage: TelegramMediaWebpage) { + init(account: Account, webPage: TelegramMediaWebpage, anchor: String? = nil) { self.account = account self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 }) self.webPage = webPage + self.anchor = anchor super.init(navigationBarTheme: nil) @@ -38,7 +40,7 @@ final class InstantPageController: ViewController { if let strongSelf = self { strongSelf.webPage = result if strongSelf.isNodeLoaded { - strongSelf.controllerNode.updateWebPage(result) + strongSelf.controllerNode.updateWebPage(result, anchor: strongSelf.anchor) } } }) @@ -82,7 +84,7 @@ final class InstantPageController: ViewController { self.displayNodeDidLoad() - self.controllerNode.updateWebPage(self.webPage) + self.controllerNode.updateWebPage(self.webPage, anchor: self.anchor) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 77d620a5ad..9a852c6e1e 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -15,8 +15,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let openPeer: (PeerId) -> Void private var webPage: TelegramMediaWebpage? + private var initialAnchor: String? private var containerLayout: ContainerViewLayout? + private var setupScrollOffsetOnLayout: Bool = false + private let statusBar: StatusBar private let navigationBar: InstantPageNavigationBar private let scrollNode: ASScrollNode @@ -73,18 +76,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar.back = navigateBack self.navigationBar.share = { [weak self] in if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { - var shareImpl: (([PeerId]) -> Void)? - let shareController = ShareController(account: account, shareAction: { peerIds in - shareImpl?(peerIds) - }, defaultAction: nil) + let shareController = ShareController(account: account, subject: .url(content.url)) strongSelf.present(shareController, nil) - shareImpl = { [weak shareController] peerIds in - shareController?.dismiss() - - for peerId in peerIds { - let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: content.url, attributes: [], media: nil, replyToMessageId: nil)]).start() - } - } } } self.navigationBar.settings = { [weak self] in @@ -186,9 +179,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.scrollNode.view.addGestureRecognizer(recognizer) } - func updateWebPage(_ webPage: TelegramMediaWebpage?) { + func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?) { if self.webPage != webPage { + self.setupScrollOffsetOnLayout = self.webPage == nil self.webPage = webPage + self.initialAnchor = anchor self.currentLayout = nil self.updateLayout() @@ -215,22 +210,38 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 let scrollInsetTop = 44.0 + statusBarHeight - let resetOffset = self.scrollNode.bounds.size.width.isZero + let resetOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout let widthUpdated = !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) + var shouldUpdateVisibleItems = false if self.scrollNode.bounds.size != layout.size || !self.scrollNode.view.contentInset.top.isEqual(to: scrollInsetTop) { self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) 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 resetOffset { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) - } if widthUpdated { self.updateLayout() } - self.updateVisibleItems() + shouldUpdateVisibleItems = true self.updateNavigationBar() } + if resetOffset { + var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) + if let anchor = self.initialAnchor, let items = self.currentLayout?.items { + self.setupScrollOffsetOnLayout = false + if !anchor.isEmpty { + outer: for item in items { + if let item = item as? InstantPageAnchorItem, item.anchor == anchor { + contentOffset = CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top) + break outer + } + } + } + } + self.scrollNode.view.contentOffset = contentOffset + } + if shouldUpdateVisibleItems { + self.updateVisibleItems() + } } private func updateLayout() { diff --git a/TelegramUI/InstantPageLayoutSpacings.swift b/TelegramUI/InstantPageLayoutSpacings.swift index 11140d2193..fcf047d89b 100644 --- a/TelegramUI/InstantPageLayoutSpacings.swift +++ b/TelegramUI/InstantPageLayoutSpacings.swift @@ -47,7 +47,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> } } else if let lower = lower { switch lower { - case .cover: + case .cover, .channelBanner: return 0.0 default: return 24.0 diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index c1b1de96a8..3b61e710d7 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -174,6 +174,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { private let callButton: HighlightableButtonNode private let nameNode: TextNode + private var verificationIconNode: ASImageNode? private let statusNode: TextNode private var inputSeparator: ASDisplayNode? @@ -255,6 +256,17 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { updatedTheme = item.theme } + var isVerified = false + if let peer = item.peer as? TelegramUser { + isVerified = peer.flags.contains(.isVerified) + } else if let peer = item.peer as? TelegramChannel { + isVerified = peer.flags.contains(.isVerified) + } + var verificationIconImage: UIImage? + if isVerified { + verificationIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.theme) + } + let displayTitle: ItemListAvatarAndNameInfoItemName if let updatingName = item.state.updatingName { displayTitle = updatingName @@ -264,11 +276,17 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { displayTitle = .title(title: "") } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + var additionalTitleInset: CGFloat = 0.0 + if let verificationIconImage = verificationIconImage { + additionalTitleInset += 3.0 + verificationIconImage.size.width + } + + let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let statusText: String let statusColor: UIColor - if let presence = item.presence as? TelegramUserPresence { + if let _ = item.peer as? TelegramUser { + let presence = (item.presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none) let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) statusText = string @@ -279,20 +297,20 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { } } else if let channel = item.peer as? TelegramChannel { if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - statusText = "\(memberCount) members" + statusText = item.strings.GroupInfo_ParticipantCount(memberCount) statusColor = item.theme.list.itemSecondaryTextColor } else { switch channel.info { case .broadcast: - statusText = "channel" + statusText = item.strings.Channel_Status statusColor = item.theme.list.itemSecondaryTextColor case .group: - statusText = "group" + statusText = item.strings.Group_Status statusColor = item.theme.list.itemSecondaryTextColor } } } else if let group = item.peer as? TelegramGroup { - statusText = "\(group.participantCount) members" + statusText = item.strings.GroupInfo_ParticipantCount(Int32(group.participantCount)) statusColor = item.theme.list.itemSecondaryTextColor } else { statusText = "" @@ -426,22 +444,6 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let _ = nameNodeApply() let _ = statusNodeApply() - /*if let _ = item.state.updatingName { - if !strongSelf.nameNode.alpha.isEqual(to: 0.5) { - strongSelf.nameNode.alpha = 0.5 - if animated { - strongSelf.nameNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.4) - } - } - } else { - if !strongSelf.nameNode.alpha.isEqual(to: 1.0) { - strongSelf.nameNode.alpha = 1.0 - if animated { - strongSelf.nameNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 0.4) - } - } - }*/ - if let peer = item.peer { strongSelf.avatarNode.setPeer(account: item.account, peer: peer, temporaryRepresentation: item.updatingImage) } @@ -450,7 +452,25 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.avatarNode.frame = avatarFrame strongSelf.updatingAvatarOverlay.frame = avatarFrame - strongSelf.nameNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) + let nameFrame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) + strongSelf.nameNode.frame = nameFrame + + if let verificationIconImage = verificationIconImage { + if strongSelf.verificationIconNode == nil { + let verificationIconNode = ASImageNode() + verificationIconNode.isLayerBacked = true + verificationIconNode.displayWithoutProcessing = true + verificationIconNode.displaysAsynchronously = false + verificationIconNode.alpha = strongSelf.nameNode.alpha + strongSelf.verificationIconNode = verificationIconNode + strongSelf.addSubnode(verificationIconNode) + } + strongSelf.verificationIconNode?.image = verificationIconImage + strongSelf.verificationIconNode?.frame = CGRect(origin: CGPoint(x: nameFrame.maxX + 3.0, y: nameFrame.minY + 4.0 + UIScreenPixel), size: verificationIconImage.size) + } else if let verificationIconNode = strongSelf.verificationIconNode { + strongSelf.verificationIconNode = nil + verificationIconNode.removeFromSupernode() + } strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size) @@ -475,7 +495,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { inputFirstField.textColor = item.theme.list.itemPrimaryTextColor inputFirstField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputFirstField.autocorrectionType = .no - inputFirstField.attributedPlaceholder = NSAttributedString(string: "First Name", font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputFirstField.attributedPlaceholder = NSAttributedString(string: item.strings.UserInfo_FirstNamePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) @@ -490,7 +510,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { inputSecondField.textColor = item.theme.list.itemPrimaryTextColor inputSecondField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputSecondField.autocorrectionType = .no - inputSecondField.attributedPlaceholder = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputSecondField.attributedPlaceholder = NSAttributedString(string: item.strings.UserInfo_LastNamePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputSecondField = inputSecondField strongSelf.view.addSubview(inputSecondField) @@ -523,7 +543,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { inputFirstField.textColor = item.theme.list.itemPrimaryTextColor inputFirstField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputFirstField.autocorrectionType = .no - inputFirstField.attributedPlaceholder = NSAttributedString(string: "Title", font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputFirstField.attributedPlaceholder = NSAttributedString(string: item.strings.GroupInfo_GroupNamePlaceholder, font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) @@ -548,10 +568,15 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.nameNode.alpha = 0.0 strongSelf.callButton.layer.animateAlpha(from: CGFloat(strongSelf.callButton.layer.opacity), to: 0.0, duration: 0.3) strongSelf.callButton.alpha = 0.0 + if let verificationIconNode = strongSelf.verificationIconNode { + verificationIconNode.layer.animateAlpha(from: CGFloat(verificationIconNode.layer.opacity), to: 0.0, duration: 0.3) + verificationIconNode.alpha = 0.0 + } } else { strongSelf.statusNode.alpha = 0.0 strongSelf.nameNode.alpha = 0.0 strongSelf.callButton.alpha = 0.0 + strongSelf.verificationIconNode?.alpha = 0.0 } } else { var animateOut = false @@ -596,10 +621,16 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.callButton.layer.animateAlpha(from: CGFloat(strongSelf.callButton.layer.opacity), to: 1.0, duration: 0.3) strongSelf.callButton.alpha = 1.0 + + if let verificationIconNode = strongSelf.verificationIconNode { + verificationIconNode.layer.animateAlpha(from: CGFloat(verificationIconNode.layer.opacity), to: 1.0, duration: 0.3) + verificationIconNode.alpha = 1.0 + } } else { strongSelf.statusNode.alpha = 1.0 strongSelf.nameNode.alpha = 1.0 strongSelf.callButton.alpha = 1.0 + strongSelf.verificationIconNode?.alpha = 1.0 } } if let presence = item.presence as? TelegramUserPresence { diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 12585c3b08..39105d7c96 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -177,7 +177,7 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV if previous?.style != state.style { updatedStyle = state.style } - return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges, mergedEntries: state.entries) + return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, mergedEntries: state.entries) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.enqueueTransition(transition) diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 08c0522d2b..3f3c50943e 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -247,7 +247,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { case let .installation(installed): rightInset += 50.0 if installed { - installationActionImage = PresentationResourcesItemList.checkIconImage(item.theme) + installationActionImage = PresentationResourcesItemList.secondaryCheckIconImage(item.theme) } else { installationActionImage = PresentationResourcesItemList.plusIconImage(item.theme) } diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index 9bc4040f92..1880630946 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -7,8 +7,11 @@ import Postbox func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { let legacyController = LegacyController(presentation: .custom) + legacyController.supportedOrientations = .portrait legacyController.statusBar.statusBarStyle = .Hide + legacyController.deferScreenEdgeGestures = [.top] + let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { controller = TGCameraController(context: legacyController.context, saveEditedPhotos: true, saveCapturedMedia: true, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) diff --git a/TelegramUI/LinkHighlightingNode.swift b/TelegramUI/LinkHighlightingNode.swift index 4944063865..e59fa36528 100644 --- a/TelegramUI/LinkHighlightingNode.swift +++ b/TelegramUI/LinkHighlightingNode.swift @@ -58,9 +58,9 @@ private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) for i in 1 ..< rects.count { topLeft.x = min(topLeft.x, rects[i].origin.x) - topLeft.y = min(topLeft.x, rects[i].origin.y) + topLeft.y = min(topLeft.y, rects[i].origin.y) bottomRight.x = max(bottomRight.x, rects[i].maxX) - bottomRight.y = max(bottomRight.x, rects[i].maxY) + bottomRight.y = max(bottomRight.y, rects[i].maxY) } topLeft.x -= inset diff --git a/TelegramUI/ListSectionHeaderNode.swift b/TelegramUI/ListSectionHeaderNode.swift index b7e430e1ab..b4a2017148 100644 --- a/TelegramUI/ListSectionHeaderNode.swift +++ b/TelegramUI/ListSectionHeaderNode.swift @@ -4,6 +4,7 @@ import Display final class ListSectionHeaderNode: ASDisplayNode { private let label: TextNode + private var actionButton: HighlightableButtonNode? private var theme: PresentationTheme var title: String? { @@ -13,6 +14,29 @@ final class ListSectionHeaderNode: ASDisplayNode { } } + var action: String? { + didSet { + if (self.action != nil) != (self.actionButton != nil) { + if let _ = self.action { + let actionButton = HighlightableButtonNode() + self.addSubnode(actionButton) + self.actionButton = actionButton + actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) + } else if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.removeFromSupernode() + } + } + if let action = self.action { + self.actionButton?.setAttributedTitle(NSAttributedString(string: action, font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), for: []) + } + self.calculatedLayoutDidChange() + self.setNeedsLayout() + } + } + + var activateAction: (() -> Void)? + init(theme: PresentationTheme) { self.theme = theme @@ -32,6 +56,9 @@ final class ListSectionHeaderNode: ASDisplayNode { self.theme = theme self.backgroundColor = theme.chatList.sectionHeaderFillColor + if let action = self.action { + self.actionButton?.setAttributedTitle(NSAttributedString(string: action, font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), for: []) + } if !self.bounds.size.width.isZero && !self.bounds.size.height.isZero { self.layout() } @@ -45,5 +72,14 @@ final class ListSectionHeaderNode: ASDisplayNode { let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil, UIEdgeInsets()) let _ = labelApply() self.label.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: labelLayout.size) + + if let actionButton = self.actionButton { + let buttonSize = actionButton.measure(CGSize(width: size.width, height: size.height)) + actionButton.frame = CGRect(origin: CGPoint(x: size.width - 9.0 - buttonSize.width, y: 6.0), size: buttonSize) + } + } + + @objc func actionButtonPressed() { + self.activateAction?() } } diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 662f076e9b..9594c1ba13 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -65,7 +65,7 @@ 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 }, automaticMediaDownloadSettings: .none) + }, presentController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, automaticMediaDownloadSettings: .none) let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) listNode.preloadPages = true diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift index a92988962a..7fd4624edd 100644 --- a/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -52,7 +52,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = true + self.listView.keepBottomItemOverscrollBackground = .white self.listView.limitHitTestToNodes = true super.init(account: account) diff --git a/TelegramUI/MessageContentKind.swift b/TelegramUI/MessageContentKind.swift new file mode 100644 index 0000000000..e1222eba8d --- /dev/null +++ b/TelegramUI/MessageContentKind.swift @@ -0,0 +1,107 @@ +import Foundation +import Postbox +import TelegramCore + +private enum MessageContentKind { + case text(String) + case image + case video + case videoMessage + case audioMessage + case sticker(String) + case animation + case file(String) + case contact + case game(String) + case location +} + +private func messageContentKind(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> MessageContentKind { + for media in message.media { + switch media { + case _ as TelegramMediaImage: + return .image + case let file as TelegramMediaFile: + var fileName: String = "" + for attribute in file.attributes { + switch attribute { + case let .Sticker(text, _, _): + return .sticker(text) + case let .FileName(name): + fileName = name + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return .audioMessage + } else { + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + return .file(title + " — " + performer) + } else if let title = title, !title.isEmpty { + return .file(title) + } else if let performer = performer, !performer.isEmpty { + return .file(performer) + } + } + case let .Video(_, _, flags): + if file.isAnimated { + return .animation + } else { + if flags.contains(.instantRoundVideo) { + return .videoMessage + } else { + return .video + } + } + default: + break + } + } + return .file(fileName) + case _ as TelegramMediaContact: + return .contact + case let game as TelegramMediaGame: + return .game(game.title) + case _ as TelegramMediaMap: + return .location + case _ as TelegramMediaAction: + return .text(plainServiceMessageString(strings: strings, message: message, accountPeerId: accountPeerId) ?? "") + default: + break + } + } + return .text(message.text) +} + +func descriptionStringForMessage(_ message: Message, strings: PresentationStrings, accountPeerId: PeerId) -> String { + switch messageContentKind(message, strings: strings, accountPeerId: accountPeerId) { + case let .text(text): + return text + case .image: + return strings.Message_Photo + case .video: + return strings.Message_Video + case .videoMessage: + return strings.Message_VideoMessage + case .audioMessage: + return strings.Message_Audio + case let .sticker(text): + if text.isEmpty { + return strings.Message_Sticker + } else { + return "\(text) \(strings.Message_Sticker)" + } + case .animation: + return strings.Message_Animation + case let .file(text): + if text.isEmpty { + return strings.Message_File + } else { + return text + } + case .contact: + return strings.Message_Contact + case let .game(text): + return text + case .location: + return strings.Message_Location + } +} diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index b90f8b55bc..bc21b3b7da 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -21,7 +21,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleTransitionNode { var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false) { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) if self.title.activity != oldValue.activity { if self.title.activity { if self.activityIndicator.layer.superlayer == nil { diff --git a/TelegramUI/NetworkUsageStatsController.swift b/TelegramUI/NetworkUsageStatsController.swift index db52bc7940..b15e32426d 100644 --- a/TelegramUI/NetworkUsageStatsController.swift +++ b/TelegramUI/NetworkUsageStatsController.swift @@ -302,71 +302,71 @@ private func networkUsageStatsControllerEntries(presentationData: PresentationDa switch section { case .cellular: - entries.append(.messagesHeader(presentationData.theme, "MESSAGES")) - entries.append(.messagesSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.generic.cellular.outgoing)))) - entries.append(.messagesReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.generic.cellular.incoming)))) + entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) + entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.generic.cellular.outgoing)))) + entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.generic.cellular.incoming)))) - entries.append(.imageHeader(presentationData.theme, "PHOTOS")) - entries.append(.imageSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.image.cellular.outgoing)))) - entries.append(.imageReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.image.cellular.incoming)))) + entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) + entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.image.cellular.outgoing)))) + entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.image.cellular.incoming)))) - entries.append(.videoHeader(presentationData.theme, "VIDEOS")) - entries.append(.videoSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.video.cellular.outgoing)))) - entries.append(.videoReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.video.cellular.incoming)))) + entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) + entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.video.cellular.outgoing)))) + entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.video.cellular.incoming)))) - entries.append(.audioHeader(presentationData.theme, "AUDIO")) - entries.append(.audioSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.audio.cellular.outgoing)))) - entries.append(.audioReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.audio.cellular.incoming)))) + entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) + entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.audio.cellular.outgoing)))) + entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.audio.cellular.incoming)))) - entries.append(.fileHeader(presentationData.theme, "DOCUMENTS")) - entries.append(.fileSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.file.cellular.outgoing)))) - entries.append(.fileReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.file.cellular.incoming)))) + entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) + entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.file.cellular.outgoing)))) + entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.file.cellular.incoming)))) - entries.append(.callHeader(presentationData.theme, "CALLS")) - entries.append(.callSent(presentationData.theme, "Bytes Sent", dataSizeString(0))) - entries.append(.callReceived(presentationData.theme, "Bytes Received", dataSizeString(0))) + entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) + entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(0))) + entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(0))) - entries.append(.reset(presentationData.theme, section, "Reset Statistics")) + entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) if stats.resetCellularTimestamp != 0 { let formatter = DateFormatter() formatter.dateFormat = "E, d MMM yyyy HH:mm" let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetCellularTimestamp))) - entries.append(.resetTimestamp(presentationData.theme, "Cellular usage since \(dateStringPlain)")) + entries.append(.resetTimestamp(presentationData.theme, presentationData.strings.NetworkUsageSettings_CellularUsageSince(dateStringPlain).0)) } case .wifi: - entries.append(.messagesHeader(presentationData.theme, "MESSAGES")) - entries.append(.messagesSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.generic.wifi.outgoing)))) - entries.append(.messagesReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.generic.wifi.incoming)))) + entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) + entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.generic.wifi.outgoing)))) + entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.generic.wifi.incoming)))) - entries.append(.imageHeader(presentationData.theme, "PHOTOS")) - entries.append(.imageSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.image.wifi.outgoing)))) - entries.append(.imageReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.image.wifi.incoming)))) + entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) + entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.image.wifi.outgoing)))) + entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.image.wifi.incoming)))) - entries.append(.videoHeader(presentationData.theme, "VIDEOS")) - entries.append(.videoSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.video.wifi.outgoing)))) - entries.append(.videoReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.video.wifi.incoming)))) + entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) + entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.video.wifi.outgoing)))) + entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.video.wifi.incoming)))) - entries.append(.audioHeader(presentationData.theme, "AUDIO")) - entries.append(.audioSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.audio.wifi.outgoing)))) - entries.append(.audioReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.audio.wifi.incoming)))) + entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) + entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.audio.wifi.outgoing)))) + entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.audio.wifi.incoming)))) - entries.append(.fileHeader(presentationData.theme, "DOCUMENTS")) - entries.append(.fileSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.file.wifi.outgoing)))) - entries.append(.fileReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.file.wifi.incoming)))) + entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) + entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(Int(stats.file.wifi.outgoing)))) + entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(Int(stats.file.wifi.incoming)))) - entries.append(.callHeader(presentationData.theme, "CALLS")) - entries.append(.callSent(presentationData.theme, "Bytes Sent", dataSizeString(0))) - entries.append(.callReceived(presentationData.theme, "Bytes Received", dataSizeString(0))) + entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) + entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(0))) + entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(0))) - entries.append(.reset(presentationData.theme, section, "Reset Statistics")) + entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) if stats.resetWifiTimestamp != 0 { let formatter = DateFormatter() formatter.dateFormat = "E, d MMM yyyy HH:mm" let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetWifiTimestamp))) - entries.append(.resetTimestamp(presentationData.theme, "Wifi usage since \(dateStringPlain)")) + entries.append(.resetTimestamp(presentationData.theme, presentationData.strings.NetworkUsageSettings_WifiUsageSince(dateStringPlain).0)) } } @@ -381,13 +381,14 @@ func networkUsageStatsController(account: Account) -> ViewController { var presentControllerImpl: ((ViewController) -> Void)? let arguments = NetworkUsageStatsControllerArguments(resetStatistics: { [weak stats] section in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Reset Statistics", color: .destructive, action: { + ActionSheetButtonItem(title: presentationData.strings.NetworkUsageSettings_ResetStats, color: .destructive, action: { dismissAction() let reset: ResetNetworkUsageStats @@ -400,7 +401,7 @@ func networkUsageStatsController(account: Account) -> ViewController { stats?.set(accountNetworkUsageStats(account: account, reset: reset)) }), ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) }) @@ -408,7 +409,7 @@ func networkUsageStatsController(account: Account) -> ViewController { let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, section.get(), stats.get()) |> deliverOnMainQueue |> map { presentationData, section, stats -> (ItemListControllerState, (ItemListNodeState, NetworkUsageStatsEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .sectionControl(["Cellular", "Wifi"], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .sectionControl([presentationData.strings.NetworkUsageSettings_Cellular, presentationData.strings.NetworkUsageSettings_Wifi], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: networkUsageStatsControllerEntries(presentationData: presentationData, section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index 53e22cabf9..cd7f969000 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -29,6 +29,7 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { case modernHeader(PresentationTheme, String) case classicHeader(PresentationTheme, String) case none(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) + case `default`(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) case sound(section: NotificationSoundSelectionSection, index: Int32, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool) var section: ItemListSectionId { @@ -39,6 +40,8 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return NotificationSoundSelectionSection.classic.rawValue case let .none(section, _, _, _): return section.rawValue + case let .default(section, _, _, _): + return section.rawValue case let .sound(section, _, _, _, _, _): return section.rawValue } @@ -57,12 +60,19 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { case .classic: return 1001 } + case let .default(section, _, _, _): + switch section { + case .modern: + return 2 + case .classic: + return 1002 + } case let .sound(section, index, _, _, _, _): switch section { case .modern: - return 2 + index + return 3 + index case .classic: - return 1002 + index + return 1003 + index } } } @@ -87,6 +97,12 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { } else { return false } + case let .default(lhsSection, lhsTheme, lhsText, lhsSelected): + if case let .default(rhsSection, rhsTheme, rhsText, rhsSelected) = rhs, lhsSection == rhsSection, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected): if case let .sound(rhsSection, rhsIndex, rhsTheme, rhsText, rhsSound, rhsSelected) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsSound == rhsSound, lhsSelected == rhsSelected { return true @@ -110,6 +126,10 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { arguments.selectSound(.none) }) + case let .default(_, theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.selectSound(.default) + }) case let .sound(_, _, theme, text, sound, selected): return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) @@ -118,11 +138,14 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { } } -private func notificationsAndSoundsEntries(presentationData: PresentationData, state: NotificationSoundSelectionState) -> [NotificationSoundSelectionEntry] { +private func notificationsAndSoundsEntries(presentationData: PresentationData, defaultSound: PeerMessageSound?, state: NotificationSoundSelectionState) -> [NotificationSoundSelectionEntry] { var entries: [NotificationSoundSelectionEntry] = [] entries.append(.modernHeader(presentationData.theme, presentationData.strings.Notifications_AlertTones)) - entries.append(.none(section: .modern, theme: presentationData.theme, text: "None", selected: state.selectedSound == .none)) + entries.append(.none(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: .none), selected: state.selectedSound == .none)) + if let defaultSound = defaultSound { + entries.append(.default(section: .modern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: .default, default: defaultSound), selected: state.selectedSound == .default)) + } for i in 0 ..< 12 { let sound: PeerMessageSound = .bundledModern(id: Int32(i)) entries.append(.sound(section: .modern, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound)) @@ -137,7 +160,87 @@ private func notificationsAndSoundsEntries(presentationData: PresentationData, s return entries } -public func notificationSoundSelectionController(account: Account, isModal: Bool, currentSound: PeerMessageSound) -> (ViewController, Signal) { +private final class AudioPlayerWrapper: NSObject, AVAudioPlayerDelegate { + private let completed: () -> Void + private var player: AVAudioPlayer? + + init(url: URL, completed: @escaping () -> Void) { + self.completed = completed + + super.init() + + self.player = try? AVAudioPlayer(contentsOf: url, fileTypeHint: "m4a") + self.player?.delegate = self + } + + func play() { + self.player?.play() + } + + func stop() { + self.player?.stop() + self.player = nil + } + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + self.completed() + } +} + +public func fileNameForNotificationSound(_ sound: PeerMessageSound, defaultSound: PeerMessageSound?) -> String { + switch sound { + case .none: + return "" + case .default: + if let defaultSound = defaultSound { + if case .default = defaultSound { + return "\(100)" + } else { + return fileNameForNotificationSound(defaultSound, defaultSound: nil) + } + } else { + return "\(100)" + } + case let .bundledModern(id): + return "\(id + 100)" + case let .bundledClassic(id): + return "\(id + 2)" + } +} + +private func playSound(account: Account, sound: PeerMessageSound, defaultSound: PeerMessageSound?) -> Signal { + if case .none = sound { + return .complete() + } else { + return Signal { subscriber in + var currentPlayer: AudioPlayerWrapper? + var deactivateImpl: (() -> Void)? + let session = account.telegramApplicationContext.mediaManager.audioSession.push(audioSessionType: .play, activate: { + if let url = Bundle.main.url(forResource: fileNameForNotificationSound(sound, defaultSound: defaultSound), withExtension: "m4a") { + currentPlayer = AudioPlayerWrapper(url: url, completed: { + deactivateImpl?() + }) + currentPlayer?.play() + } + }, deactivate: { + currentPlayer?.stop() + currentPlayer = nil + + return .complete() + }) + deactivateImpl = { + session.dispose() + } + return ActionDisposable { + session.dispose() + currentPlayer?.stop() + currentPlayer = nil + } + } |> runOn(Queue.mainQueue()) + } +} + +public func notificationSoundSelectionController(account: Account, isModal: Bool, currentSound: PeerMessageSound, defaultSound: PeerMessageSound?, completion: @escaping (PeerMessageSound) -> Void) -> ViewController { let statePromise = ValuePromise(NotificationSoundSelectionState(selectedSound: currentSound), ignoreRepeated: true) let stateValue = Atomic(value: NotificationSoundSelectionState(selectedSound: currentSound)) let updateState: ((NotificationSoundSelectionState) -> NotificationSoundSelectionState) -> Void = { f in @@ -147,10 +250,14 @@ public func notificationSoundSelectionController(account: Account, isModal: Bool var completeImpl: (() -> Void)? var cancelImpl: (() -> Void)? + let playSoundDisposable = MetaDisposable() + let arguments = NotificationSoundSelectionArguments(account: account, selectSound: { sound in updateState { state in return NotificationSoundSelectionState(selectedSound: sound) } + + playSoundDisposable.set(playSound(account: account, sound: sound, defaultSound: defaultSound).start()) }, complete: { completeImpl?() }, cancel: { @@ -169,28 +276,27 @@ public func notificationSoundSelectionController(account: Account, isModal: Bool }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_TextTone), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(presentationData: presentationData, state: state), style: .blocks) + let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(presentationData: presentationData, defaultSound: defaultSound, state: state), style: .blocks) return (controllerState, (listState, arguments)) } - let controller = ItemListController(account: account, state: signal) + let controller = ItemListController(account: account, state: signal |> afterDisposed { + playSoundDisposable.dispose() + }) controller.enableInteractiveDismiss = true - let result = Promise() - completeImpl = { [weak controller] in let sound = stateValue.with { state in return state.selectedSound } - result.set(.single(sound)) + completion(sound) controller?.dismiss() } cancelImpl = { [weak controller] in - result.set(.single(nil)) controller?.dismiss() } - return (controller, result.get()) + return controller } diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 8396cf0aa6..4b79e96766 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -237,13 +237,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .messageSound(theme, text, value, sound): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { - let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound) + let controller = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in + arguments?.updateMessageSound(value) + }) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in - if let value = value { - arguments?.updateMessageSound(value) - } - })) }) case let .messageNotice(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) @@ -259,13 +256,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .groupSound(theme, text, value, sound): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { - let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound) + let controller = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in + arguments?.updateGroupSound(value) + }) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in - if let value = value { - arguments?.updateGroupSound(value) - } - })) }) case let .groupNotice(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) @@ -293,19 +287,27 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } } +private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound { + if case .default = sound { + return .bundledModern(id: 0) + } else { + return sound + } +} + private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { var entries: [NotificationsAndSoundsEntry] = [] entries.append(.messageHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications)) entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled)) entries.append(.messagePreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.privateChats.displayPreviews)) - entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: globalSettings.privateChats.sound), globalSettings.privateChats.sound)) + entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.privateChats.sound)), filteredGlobalSound(globalSettings.privateChats.sound))) entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsHelp)) entries.append(.groupHeader(presentationData.theme, presentationData.strings.Notifications_GroupNotifications)) entries.append(.groupAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.groupChats.enabled)) entries.append(.groupPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.groupChats.displayPreviews)) - entries.append(.groupSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: globalSettings.groupChats.sound), globalSettings.groupChats.sound)) + entries.append(.groupSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.groupChats.sound)), filteredGlobalSound(globalSettings.groupChats.sound))) entries.append(.groupNotice(presentationData.theme, presentationData.strings.Notifications_GroupNotificationsHelp)) entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications)) @@ -373,9 +375,10 @@ public func notificationsAndSoundsController(account: Account) -> ViewController return settings.withUpdatedDisplayPreviews(value) }).start() }, resetNotifications: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Reset", color: .destructive, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Notifications_Reset, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let modifyPeers = account.postbox.modify { modifier -> Void in @@ -389,7 +392,7 @@ public func notificationsAndSoundsController(account: Account) -> ViewController let _ = signal.start() }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -415,7 +418,7 @@ public func notificationsAndSoundsController(account: Account) -> ViewController inAppSettings = InAppNotificationSettings.defaultSettings } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Notifications"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(globalSettings: viewSettings, inAppSettings: inAppSettings, presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/NumericFormat.swift b/TelegramUI/NumericFormat.swift index 46aff24ef1..da511ff77f 100644 --- a/TelegramUI/NumericFormat.swift +++ b/TelegramUI/NumericFormat.swift @@ -29,8 +29,12 @@ func timeIntervalString(strings: PresentationStrings, value: Int32) -> String { return strings.MessageTimer_Hours(max(1, value / (60 * 60))) } else if value < 60 * 60 * 24 * 7 { return strings.MessageTimer_Days(max(1, value / (60 * 60 * 24))) - } else { + } else if value < 60 * 60 * 24 * 30 { return strings.MessageTimer_Weeks(max(1, value / (60 * 60 * 24 * 7))) + } else if value < 60 * 60 * 24 * 360 { + return strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))) + } else { + return strings.MessageTimer_Years(max(1, value / (60 * 60 * 24 * 365))) } } diff --git a/TelegramUI/OverlayUniversalVideoNode.swift b/TelegramUI/OverlayUniversalVideoNode.swift index f5656f7587..cfa7961021 100644 --- a/TelegramUI/OverlayUniversalVideoNode.swift +++ b/TelegramUI/OverlayUniversalVideoNode.swift @@ -7,6 +7,7 @@ import TelegramCore final class OverlayUniversalVideoNode: OverlayMediaItemNode { private let content: UniversalVideoContent private let videoNode: UniversalVideoNode + private let decoration: OverlayVideoDecoration private var validLayoutSize: CGSize? @@ -14,11 +15,18 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { return OverlayMediaItemNodeGroup(rawValue: 0) } + override var isMinimizeable: Bool { + return true + } + init(account: Account, manager: UniversalVideoContentManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content + var unminimizeImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? var closeImpl: (() -> Void)? - let decoration = OverlayVideoDecoration(togglePlayPause: { + let decoration = OverlayVideoDecoration(unminimize: { + unminimizeImpl?() + }, togglePlayPause: { togglePlayPauseImpl?() }, expand: { expand() @@ -26,9 +34,13 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { closeImpl?() }) self.videoNode = UniversalVideoNode(account: account, manager: manager, decoration: decoration, content: content, priority: .overlay) + self.decoration = decoration super.init() + unminimizeImpl = { [weak self] in + self?.unminimize?() + } togglePlayPauseImpl = { [weak self] in self?.videoNode.togglePlayPause() } @@ -80,4 +92,8 @@ final class OverlayUniversalVideoNode: OverlayMediaItemNode { self.videoNode.frame = CGRect(origin: CGPoint(), size: size) self.videoNode.updateLayout(size: size, transition: .immediate) } + + override func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { + self.decoration.updateMinimizedEdge(edge, adjusting: adjusting) + } } diff --git a/TelegramUI/OverlayVideoDecoration.swift b/TelegramUI/OverlayVideoDecoration.swift index ca1b687aa8..11793afc58 100644 --- a/TelegramUI/OverlayVideoDecoration.swift +++ b/TelegramUI/OverlayVideoDecoration.swift @@ -3,6 +3,22 @@ import AsyncDisplayKit import Display import SwiftSignalKit +import LegacyComponents + +private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdge, view: TGEmbedPIPPullArrowView) { + let arrowX: CGFloat + switch edge { + case .left: + view.transform = .identity + arrowX = size.width - 40.0 + floor((40.0 - view.bounds.size.width) / 2.0) + case .right: + view.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) + arrowX = floor((40.0 - view.bounds.size.width) / 2.0) + } + + view.frame = CGRect(origin: CGPoint(x: arrowX, y: floor((size.height - view.bounds.size.height) / 2.0)), size: view.bounds.size) +} + private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayPlainVideoShadow")?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(top: 22.0, left: 25.0, bottom: 26.0, right: 25.0), resizingMode: .stretch) final class OverlayVideoDecoration: UniversalVideoDecoration { @@ -10,14 +26,22 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { let contentContainerNode: ASDisplayNode let foregroundNode: ASDisplayNode? + private let unminimize: () -> Void + private let shadowNode: ASImageNode + private let foregroundContainerNode: ASDisplayNode private let controlsNode: PictureInPictureVideoControlsNode + private var minimizedBlurView: UIVisualEffectView? + private var minimizedArrowView: TGEmbedPIPPullArrowView? + private var minimizedEdge: OverlayMediaItemMinimizationEdge? private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private var validLayoutSize: CGSize? - init(togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void) { + init(unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void) { + self.unminimize = unminimize + self.shadowNode = ASImageNode() self.shadowNode.image = backgroundImage self.backgroundNode = self.shadowNode @@ -33,7 +57,10 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { close() }) self.controlsNode.alpha = 0.0 - self.foregroundNode = self.controlsNode + + self.foregroundContainerNode = ASDisplayNode() + self.foregroundContainerNode.addSubnode(self.controlsNode) + self.foregroundNode = self.foregroundContainerNode //self.controlsNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(controlsNodeTapGesture(_:)))) } @@ -67,9 +94,19 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom))) + transition.updateFrame(node: self.foregroundContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.controlsNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) self.controlsNode.updateLayout(size: size, transition: transition) + if let minimizedBlurView = self.minimizedBlurView { + minimizedBlurView.frame = CGRect(origin: CGPoint(), size: size) + } + + if let minimizedArrowView = self.minimizedArrowView, let minimizedEdge = self.minimizedEdge { + setupArrowFrame(size: size, edge: minimizedEdge, view: minimizedArrowView) + } + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) if let contentNode = self.contentNode { transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -78,12 +115,16 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { } func tap() { - if self.controlsNode.alpha.isZero { - self.controlsNode.alpha = 1.0 - self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if self.minimizedEdge != nil { + self.unminimize() } else { - self.controlsNode.alpha = 0.0 - self.controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + if self.controlsNode.alpha.isZero { + self.controlsNode.alpha = 1.0 + self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } else { + self.controlsNode.alpha = 0.0 + self.controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } } } @@ -96,4 +137,68 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { } } } + + func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { + if self.minimizedEdge == edge { + if let minimizedArrowView = self.minimizedArrowView { + minimizedArrowView.setAngled(!adjusting, animated: true) + } + return + } + + self.minimizedEdge = edge + + if let edge = edge { + if self.minimizedBlurView == nil { + let minimizedBlurView = UIVisualEffectView(effect: nil) + self.minimizedBlurView = minimizedBlurView + if let validLayoutSize = self.validLayoutSize { + minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + } + minimizedBlurView.isHidden = true + self.foregroundContainerNode.view.addSubview(minimizedBlurView) + } + if self.minimizedArrowView == nil { + let minimizedArrowView = TGEmbedPIPPullArrowView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 8.0, height: 38.0))) + minimizedArrowView.alpha = 0.0 + self.minimizedArrowView = minimizedArrowView + self.minimizedBlurView?.contentView.addSubview(minimizedArrowView) + } + if let minimizedArrowView = self.minimizedArrowView { + if let validLayoutSize = self.validLayoutSize { + setupArrowFrame(size: validLayoutSize, edge: edge, view: minimizedArrowView) + } + minimizedArrowView.setAngled(!adjusting, animated: true) + } + } + + let effect: UIBlurEffect? = edge != nil ? UIBlurEffect(style: .light) : nil + if true { + if let edge = edge { + self.minimizedBlurView?.isHidden = false + + switch edge { + case .left: + break + case .right: + break + } + } + + UIView.animate(withDuration: 0.35, animations: { + self.minimizedBlurView?.effect = effect + self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0; + }, completion: { [weak self] finished in + if let strongSelf = self { + if finished && edge == nil { + strongSelf.minimizedBlurView?.isHidden = true + } + } + }) + } else { + self.minimizedBlurView?.effect = effect; + self.minimizedBlurView?.isHidden = edge == nil + self.minimizedArrowView?.alpha = edge != nil ? 1.0 : 0.0 + } + } } diff --git a/TelegramUI/PeerAvatarImageGalleryItem.swift b/TelegramUI/PeerAvatarImageGalleryItem.swift index 34a93c4968..7b2c461e87 100644 --- a/TelegramUI/PeerAvatarImageGalleryItem.swift +++ b/TelegramUI/PeerAvatarImageGalleryItem.swift @@ -7,19 +7,21 @@ import TelegramCore class PeerAvatarImageGalleryItem: GalleryItem { let account: Account + let strings: PresentationStrings let entry: AvatarGalleryEntry - init(account: Account, entry: AvatarGalleryEntry) { + init(account: Account, strings: PresentationStrings, entry: AvatarGalleryEntry) { self.account = account + self.strings = strings self.entry = entry } func node() -> GalleryItemNode { let node = PeerAvatarImageGalleryItemNode(account: self.account) - /*if let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - }*/ + if let indexData = self.entry.indexData { + node._title.set(.single("\(indexData.position + 1) \(self.strings.Common_of) \(indexData.totalCount)")) + } node.setEntry(self.entry) @@ -28,9 +30,9 @@ class PeerAvatarImageGalleryItem: GalleryItem { func updateNode(node: GalleryItemNode) { if let node = node as? PeerAvatarImageGalleryItemNode { - /*if let location = self.location { - node._title.set(.single("\(location.index + 1) of \(location.count)")) - }*/ + if let indexData = self.entry.indexData { + node._title.set(.single("\(indexData.position + 1) \(self.strings.Common_of) \(indexData.totalCount)")) + } node.setEntry(self.entry) } @@ -102,7 +104,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -142,7 +144,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index 3de988e277..a122805d85 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -176,7 +176,7 @@ func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> break } if let tagMask = tagMask { - return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) + return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, clipHoles: false, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> map { (view, _, _) -> AudioPlaylistItem? in var index = 0 diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 21415476cc..9a6725d0a6 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -28,7 +28,6 @@ public class PeerMediaCollectionController: ViewController { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() - private var titleView: PeerMediaCollectionTitleView? private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -48,14 +47,8 @@ public class PeerMediaCollectionController: ViewController { 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 - self.navigationItem.titleView = self.titleView - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.ready.set(.never()) @@ -68,8 +61,9 @@ public class PeerMediaCollectionController: ViewController { let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded { + let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(id) var galleryMedia: Media? - if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(id) { + if let message = galleryMessage?.message { for media in message.media { if let file = media as? TelegramMediaFile { galleryMedia = file @@ -85,59 +79,38 @@ public class PeerMediaCollectionController: ViewController { } } - if let galleryMedia = galleryMedia { - if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" { + if let galleryMessage = galleryMessage, let galleryMedia = galleryMedia { + if let file = galleryMedia as? TelegramMediaFile, file.isVoice || file.isMusic { } else { - let gallery = GalleryController(account: strongSelf.account, messageId: id, replaceRootController: { controller, ready in + let _ = (storedMessageFromSearch(account: strongSelf.account, message: galleryMessage.message) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: nil) - - strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in - if let strongSelf = strongSelf { - if let messageIdAndMedia = messageIdAndMedia { - strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]] - } else { - strongSelf.controllerInteraction?.hiddenMedia = [:] - } - strongSelf.mediaCollectionDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateHiddenMedia() - } else if let itemNode = itemNode as? ListMessageNode { - itemNode.updateHiddenMedia() - } else if let itemNode = itemNode as? GridMessageItemNode { - itemNode.updateHiddenMedia() + let gallery = GalleryController(account: strongSelf.account, messageId: id, replaceRootController: { controller, ready in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.replaceTopController(controller, animated: false, ready: ready) } - } - } - })) - - strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in - if let strongSelf = self { - var transitionNode: ASDisplayNode? - strongSelf.mediaCollectionDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - if let result = itemNode.transitionNode(id: messageId, media: media) { - transitionNode = result + }, baseNavigationController: nil) + strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in + if let strongSelf = strongSelf { + if let messageIdAndMedia = messageIdAndMedia { + strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]] + } else { + strongSelf.controllerInteraction?.hiddenMedia = [:] } - } else if let itemNode = itemNode as? ListMessageNode { - if let result = itemNode.transitionNode(id: messageId, media: media) { - transitionNode = result - } - } else if let itemNode = itemNode as? GridMessageItemNode { - if let result = itemNode.transitionNode(id: messageId, media: media) { - transitionNode = result + strongSelf.mediaCollectionDisplayNode.updateHiddenMedia() + } + })) + + strongSelf.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in + if let strongSelf = self { + if let transitionNode = strongSelf.mediaCollectionDisplayNode.transitionNodeForGallery(messageId: messageId, media: media) { + return GalleryTransitionArguments(transitionNode: transitionNode, transitionContainerNode: strongSelf.mediaCollectionDisplayNode, transitionBackgroundNode: strongSelf.mediaCollectionDisplayNode.historyNode as! ASDisplayNode) } } - } - if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: transitionNode, transitionContainerNode: strongSelf.mediaCollectionDisplayNode, transitionBackgroundNode: strongSelf.mediaCollectionDisplayNode.historyNode as! ASDisplayNode) - } + return nil + })) } - return nil - })) + }) } } } @@ -208,6 +181,8 @@ public class PeerMediaCollectionController: ViewController { }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in + }, openSearch: { [weak self] in + self?.activateSearch() }, automaticMediaDownloadSettings: .none) self.controllerInteraction = controllerInteraction @@ -369,7 +344,9 @@ public class PeerMediaCollectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerMediaCollectionControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, interfaceInteraction: self.interfaceInteraction!) + self.displayNode = PeerMediaCollectionControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, interfaceInteraction: self.interfaceInteraction!, navigationBar: self.navigationBar, requestDeactivateSearch: { [weak self] in + self?.deactivateSearch() + }) self.ready.set(combineLatest(self.mediaCollectionDisplayNode.historyNode.historyState.get(), self._peerReady.get()) |> map { $1 }) @@ -409,7 +386,6 @@ public class PeerMediaCollectionController: ViewController { if self.isNodeLoaded { self.mediaCollectionDisplayNode.updateMediaCollectionInterfaceState(updatedInterfaceState, animated: animated) - self.titleView?.updateMediaCollectionInterfaceState(updatedInterfaceState, animated: animated) } self.interfaceState = updatedInterfaceState @@ -450,4 +426,21 @@ public class PeerMediaCollectionController: ViewController { self.updateInterfaceState(animated: true, { $0.withSelectionState() }) } } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + self.mediaCollectionDisplayNode.activateSearch() + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.mediaCollectionDisplayNode.deactivateSearch() + } + } } diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 7c83c39d44..4787196e50 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -5,7 +5,12 @@ import SwiftSignalKit import Display import TelegramCore -private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) -> ASDisplayNode { +struct PeerMediaCollectionMessageForGallery { + let message: Message + let fromSearchResults: Bool +} + +private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) -> ChatHistoryNode & ASDisplayNode { switch mode { case .photoOrVideo: return ChatHistoryGridNode(account: account, peerId: peerId, messageId: messageId, tagMask: .photoOrVideo, controllerInteraction: controllerInteraction) @@ -24,18 +29,51 @@ private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Ac } } +private func updateLoadNodeState(_ node: PeerMediaCollectionEmptyNode, _ loadState: ChatHistoryNodeLoadState?) { + if let loadState = loadState { + switch loadState { + case .messages: + node.isHidden = true + node.isLoading = false + case .empty: + node.isHidden = false + node.isLoading = false + case .loading: + node.isHidden = false + node.isLoading = true + } + } else { + node.isHidden = false + node.isLoading = true + } +} + +private func tagMaskForMode(_ mode: PeerMediaCollectionMode) -> MessageTags { + switch mode { + case .photoOrVideo: + return .photoOrVideo + case .file: + return .file + case .music: + return .music + case .webpage: + return .webPage + } +} + class PeerMediaCollectionControllerNode: ASDisplayNode { private let account: Account private let peerId: PeerId private let controllerInteraction: ChatControllerInteraction private let interfaceInteraction: ChatPanelInterfaceInteraction + private let navigationBar: NavigationBar? private let sectionsNode: PeerMediaCollectionSectionsNode - private var historyNodeImpl: ASDisplayNode - var historyNode: ChatHistoryNode { - return self.historyNodeImpl as! ChatHistoryNode - } + private(set) var historyNode: ChatHistoryNode & ASDisplayNode + private var historyEmptyNode: PeerMediaCollectionEmptyNode + + private var searchDisplayController: SearchDisplayController? private let candidateHistoryNodeReadyDisposable = MetaDisposable() private var candidateHistoryNode: (ASDisplayNode, PeerMediaCollectionMode)? @@ -44,6 +82,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } var requestUpdateMediaCollectionInterfaceState: (Bool, (PeerMediaCollectionInterfaceState) -> PeerMediaCollectionInterfaceState) -> Void = { _, _ in } + let requestDeactivateSearch: () -> Void private var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState @@ -54,20 +93,25 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { private var presentationData: PresentationData - init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction, navigationBar: NavigationBar?, requestDeactivateSearch: @escaping () -> Void) { self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction self.interfaceInteraction = interfaceInteraction + self.navigationBar = navigationBar + + self.requestDeactivateSearch = requestDeactivateSearch 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.historyNode = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction) + self.historyEmptyNode = PeerMediaCollectionEmptyNode(mode: self.mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) + self.historyEmptyNode.isHidden = true - self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, accountPeerId: account.peerId) super.init() @@ -75,10 +119,14 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { return UITracingLayerView() }) - self.historyNodeImpl.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.historyNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.addSubnode(self.historyNodeImpl) + self.addSubnode(self.historyNode) + self.addSubnode(self.historyEmptyNode) + if let navigationBar = navigationBar { + self.addSubnode(navigationBar) + } self.addSubnode(self.sectionsNode) self.sectionsNode.indexUpdated = { [weak self] index in @@ -99,6 +147,13 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { strongSelf.requestUpdateMediaCollectionInterfaceState(true, { $0.withMode(mode) }) } } + + updateLoadNodeState(self.historyEmptyNode, self.historyNode.loadState) + self.historyNode.setLoadStateUpdated { [weak self] loadState in + if let strongSelf = self { + updateLoadNodeState(strongSelf.historyEmptyNode, loadState) + } + } } deinit { @@ -108,13 +163,31 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) { self.containerLayout = (layout, navigationBarHeight) - var insets = layout.insets(options: [.input]) - insets.top += navigationBarHeight + var vanillaInsets = layout.insets(options: []) + vanillaInsets.top += navigationBarHeight + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + vanillaInsets.top += 20.0 + } + } 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))) + var sectionOffset: CGFloat = 0.0 + if navigationBarHeight.isZero { + sectionOffset = -sectionsHeight + } + transition.updateFrame(node: self.sectionsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + sectionOffset), size: CGSize(width: layout.size.width, height: sectionsHeight))) - insets.top += sectionsHeight + var insets = vanillaInsets + if !navigationBarHeight.isZero { + insets.top += sectionsHeight + } + + if let inputHeight = layout.inputHeight { + insets.bottom += inputHeight + } if let selectionState = self.mediaCollectionInterfaceState.selectionState { let interfaceState = self.chatPresentationInterfaceState.updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) @@ -156,10 +229,13 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { } } - let previousBounds = self.historyNodeImpl.bounds - self.historyNodeImpl.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) - self.historyNodeImpl.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - + let previousBounds = self.historyNode.bounds + self.historyNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) + self.historyNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + self.historyEmptyNode.updateLayout(size: layout.size, insets: vanillaInsets, transition: transition) + transition.updateFrame(node: self.historyEmptyNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + let listViewCurve: ListViewAnimationCurve if curve == 7 { listViewCurve = .Spring(duration: duration) @@ -204,7 +280,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { modeSelectionNode.frame = CGRect(origin: CGPoint(), size: layout.size) modeSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) modeSelectionNode.mediaCollectionInterfaceState = self.mediaCollectionInterfaceState - self.insertSubnode(modeSelectionNode, aboveSubnode: self.historyNodeImpl) + self.insertSubnode(modeSelectionNode, aboveSubnode: self.historyNode) modeSelectionNode.animateIn() self.modeSelectionNode = modeSelectionNode } @@ -216,16 +292,79 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { } } + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { + return + } + + var maybePlaceholderNode: SearchBarPlaceholderNode? + if let listNode = historyNode as? ListView { + listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatHistorySearchContainerNode(account: self.account, peerId: self.peerId, tagMask: tagMaskForMode(self.mediaCollectionInterfaceState.mode), interfaceInteraction: self.controllerInteraction), cancel: { [weak self] in + self?.requestDeactivateSearch() + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + self.searchDisplayController = nil + var maybePlaceholderNode: SearchBarPlaceholderNode? + if let listNode = self.historyNode as? ListView { + listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + } + } + func updateMediaCollectionInterfaceState(_ mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState, animated: Bool) { if self.mediaCollectionInterfaceState != mediaCollectionInterfaceState { if self.mediaCollectionInterfaceState.mode != mediaCollectionInterfaceState.mode { + let previousMode = self.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]) - insets.top += containerLayout.1 + var vanillaInsets = containerLayout.0.insets(options: []) + vanillaInsets.top += containerLayout.1 + + if let searchDisplayController = self.searchDisplayController { + if !searchDisplayController.isDeactivating { + vanillaInsets.top += 20.0 + } + } + + var insets = vanillaInsets + + if !containerLayout.1.isZero { + insets.top += self.sectionsNode.bounds.size.height + } + + if let inputHeight = containerLayout.0.inputHeight { + insets.bottom += inputHeight + } let previousBounds = node.bounds node.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: containerLayout.0.size.width, height: containerLayout.0.size.height) @@ -236,20 +375,46 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { additionalBottomInset = selectionPanel.bounds.size.height } - (node as! ChatHistoryNode).updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left), duration: 0.0, curve: .Default)) + node.updateLayout(transition: .immediate, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: containerLayout.0.size, insets: UIEdgeInsets(top: insets.top, left: insets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left), duration: 0.0, curve: .Default)) - self.candidateHistoryNodeReadyDisposable.set(((node as! ChatHistoryNode).historyState.get() + let historyEmptyNode = PeerMediaCollectionEmptyNode(mode: mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) + historyEmptyNode.isHidden = true + historyEmptyNode.updateLayout(size: containerLayout.0.size, insets: vanillaInsets, transition: .immediate) + historyEmptyNode.frame = CGRect(origin: CGPoint(), size: containerLayout.0.size) + + self.candidateHistoryNodeReadyDisposable.set((node.historyState.get() |> 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, belowSubnode: strongSelf.historyNodeImpl) + strongSelf.insertSubnode(strongNode, belowSubnode: strongSelf.historyNode) + strongSelf.insertSubnode(historyEmptyNode, aboveSubnode: strongNode) - let previousNode = strongSelf.historyNodeImpl - strongSelf.historyNodeImpl = strongNode + let previousNode = strongSelf.historyNode + let previousEmptyNode = strongSelf.historyEmptyNode + strongSelf.historyNode = strongNode + strongSelf.historyEmptyNode = historyEmptyNode + updateLoadNodeState(strongSelf.historyEmptyNode, strongSelf.historyNode.loadState) + strongSelf.historyNode.setLoadStateUpdated { loadState in + if let strongSelf = self { + updateLoadNodeState(strongSelf.historyEmptyNode, loadState) + } + } - previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in + let directionMultiplier: CGFloat + if previousMode.rawValue < mediaCollectionInterfaceState.mode.rawValue { + directionMultiplier = 1.0 + } else { + directionMultiplier = -1.0 + } + + previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * strongSelf.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in previousNode?.removeFromSupernode() }) + previousEmptyNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * strongSelf.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousEmptyNode] _ in + previousEmptyNode?.removeFromSupernode() + }) + strongSelf.historyNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * strongSelf.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + strongSelf.historyEmptyNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * strongSelf.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } })) } @@ -264,4 +429,64 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } + + func updateHiddenMedia() { + self.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHiddenMedia() + } else if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } else if let itemNode = itemNode as? GridMessageItemNode { + itemNode.updateHiddenMedia() + } + } + + if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + searchContentNode.updateHiddenMedia() + } + } + + func messageForGallery(_ id: MessageId) -> PeerMediaCollectionMessageForGallery? { + if let message = self.historyNode.messageInCurrentHistoryView(id) { + return PeerMediaCollectionMessageForGallery(message: message, fromSearchResults: false) + } + + if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let message = searchContentNode.messageForGallery(id) { + return PeerMediaCollectionMessageForGallery(message: message, fromSearchResults: true) + } + } + + return nil + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> ASDisplayNode? { + if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let transitionNode = searchContentNode.transitionNodeForGallery(messageId: messageId, media: media) { + return transitionNode + } + } + + var transitionNode: ASDisplayNode? + self.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } else if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } else if let itemNode = itemNode as? GridMessageItemNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + if let transitionNode = transitionNode { + return transitionNode + } + + return nil + } } diff --git a/TelegramUI/PeerMediaCollectionEmptyNode.swift b/TelegramUI/PeerMediaCollectionEmptyNode.swift new file mode 100644 index 0000000000..a61e440684 --- /dev/null +++ b/TelegramUI/PeerMediaCollectionEmptyNode.swift @@ -0,0 +1,97 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class PeerMediaCollectionEmptyNode: ASDisplayNode { + private let mode: PeerMediaCollectionMode + + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let iconNode: ASImageNode + private let textNode: ASTextNode + + private let activityIndicator: ActivityIndicator + + var isLoading: Bool = false { + didSet { + if self.isLoading != oldValue { + if self.isLoading { + self.iconNode.isHidden = true + self.textNode.isHidden = true + self.addSubnode(self.activityIndicator) + } else { + self.iconNode.isHidden = false + self.textNode.isHidden = false + self.activityIndicator.removeFromSupernode() + } + } + } + } + + init(mode: PeerMediaCollectionMode, theme: PresentationTheme, strings: PresentationStrings) { + self.mode = mode + self.theme = theme + self.strings = strings + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + + self.textNode = ASTextNode() + self.textNode.isLayerBacked = true + self.textNode.displaysAsynchronously = false + + self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemSecondaryTextColor), speed: .regular) + + let icon: UIImage? + let text: NSAttributedString + switch mode { + case .photoOrVideo: + icon = UIImage(bundleImageName: "Media Grid/Empty List Placeholders/ImagesAndVideo")?.precomposed() + let string1 = NSAttributedString(string: strings.SharedMedia_EmptyTitle, font: Font.medium(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + let string2 = NSAttributedString(string: "\n\n\(strings.SharedMedia_EmptyText)", font: Font.regular(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + let string = NSMutableAttributedString() + string.append(string1) + string.append(string2) + text = string + case .file: + icon = UIImage(bundleImageName: "Media Grid/Empty List Placeholders/Files")?.precomposed() + text = NSAttributedString(string: strings.SharedMedia_EmptyFilesText, font: Font.regular(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + case .webpage: + icon = UIImage(bundleImageName: "Media Grid/Empty List Placeholders/Links")?.precomposed() + text = NSAttributedString(string: strings.SharedMedia_EmptyLinksText, font: Font.regular(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + case .music: + icon = UIImage(bundleImageName: "Media Grid/Empty List Placeholders/Music")?.precomposed() + text = NSAttributedString(string: strings.SharedMedia_EmptyMusicText, font: Font.regular(16.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + } + self.iconNode.image = icon + self.textNode.attributedText = text + + super.init() + + self.backgroundColor = theme.list.plainBackgroundColor + + self.addSubnode(self.iconNode) + self.addSubnode(self.textNode) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) + + let textSize = self.textNode.measure(CGSize(width: size.width - 20.0, height: size.height)) + + if let image = self.iconNode.image { + let imageSpacing: CGFloat = 22.0 + let contentHeight = image.size.height + imageSpacing + textSize.height + var contentRect = CGRect(origin: CGPoint(x: displayRect.minX, y: displayRect.minY + floor((displayRect.height - contentHeight) / 2.0)), size: CGSize(width: displayRect.width, height: contentHeight)) + contentRect.origin.y = max(displayRect.minY + 39.0, floor(contentRect.origin.y - contentRect.height * 0.0)) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - image.size.width) / 2.0), y: contentRect.minY), size: image.size)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: contentRect.maxY - textSize.height), size: textSize)) + } + + let activitySize = self.activityIndicator.measure(size) + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0)), size: activitySize)) + } +} diff --git a/TelegramUI/PeerMediaCollectionInterfaceState.swift b/TelegramUI/PeerMediaCollectionInterfaceState.swift index 2dc5a66637..8d17b2fc7a 100644 --- a/TelegramUI/PeerMediaCollectionInterfaceState.swift +++ b/TelegramUI/PeerMediaCollectionInterfaceState.swift @@ -1,11 +1,11 @@ import Foundation import Postbox -enum PeerMediaCollectionMode { +enum PeerMediaCollectionMode: Int32 { case photoOrVideo case file - case music case webpage + case music } func titleForPeerMediaCollectionMode(_ mode: PeerMediaCollectionMode, strings: PresentationStrings) -> String { diff --git a/TelegramUI/PeerMediaCollectionSectionsNode.swift b/TelegramUI/PeerMediaCollectionSectionsNode.swift index 5c0cf81374..bdb3c6b943 100644 --- a/TelegramUI/PeerMediaCollectionSectionsNode.swift +++ b/TelegramUI/PeerMediaCollectionSectionsNode.swift @@ -40,12 +40,12 @@ final class PeerMediaCollectionSectionsNode: ASDisplayNode { } func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let panelHeight: CGFloat = 44.0 + let panelHeight: CGFloat = 39.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)) + self.segmentedControl.frame = CGRect(origin: CGPoint(x: sideInset, y: panelHeight - 11.0 - controlHeight), 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))) diff --git a/TelegramUI/PeerMediaCollectionTitleView.swift b/TelegramUI/PeerMediaCollectionTitleView.swift deleted file mode 100644 index 2937d80c97..0000000000 --- a/TelegramUI/PeerMediaCollectionTitleView.swift +++ /dev/null @@ -1,95 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display - -final class PeerMediaCollectionTitleView: UIView { - private let toggle: () -> Void - - private let titleNode: ASTextNode - private let arrowView: UIImageView - private let button: HighlightTrackingButton - - private var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState - - init(mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState, toggle: @escaping () -> Void) { - self.mediaCollectionInterfaceState = mediaCollectionInterfaceState - self.toggle = toggle - - self.titleNode = ASTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.truncationMode = .byTruncatingTail - self.titleNode.isOpaque = false - - self.arrowView = UIImageView(image: PresentationResourcesRootController.navigationDropdownArrowImage(self.mediaCollectionInterfaceState.theme)) - - self.button = HighlightTrackingButton(frame: CGRect()) - - super.init(frame: CGRect()) - - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mediaCollectionInterfaceState.mode, strings: self.mediaCollectionInterfaceState.strings), font: Font.medium(17.0), textColor: self.mediaCollectionInterfaceState.theme.rootController.navigationBar.primaryTextColor) - self.addSubnode(self.titleNode) - self.addSubview(self.arrowView) - - self.button.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.arrowView.layer.removeAnimation(forKey: "opacity") - strongSelf.titleNode.alpha = 0.4 - strongSelf.arrowView.alpha = 0.4 - } else { - strongSelf.titleNode.alpha = 1.0 - strongSelf.arrowView.alpha = 1.0 - strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.arrowView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) - self.addSubview(self.button) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - let size = self.bounds.size - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - let arrowSize = self.arrowView.bounds.size - let titleArrowSpacing: CGFloat = 4.0 - let titleSize = self.titleNode.measure(CGSize(width: size.width - arrowSize.width - titleArrowSpacing, height: size.height)) - - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) - self.titleNode.frame = titleFrame - self.arrowView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleArrowSpacing, y: titleFrame.minY + floor((titleSize.height - arrowSize.height) / 2.0 + 2.0)), size: arrowSize) - } - - func updateMediaCollectionInterfaceState(_ mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState, animated: Bool) { - if self.mediaCollectionInterfaceState != mediaCollectionInterfaceState { - if mediaCollectionInterfaceState.mode != self.mediaCollectionInterfaceState.mode { - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mediaCollectionInterfaceState.mode, strings: mediaCollectionInterfaceState.strings), font: Font.medium(17.0), textColor: mediaCollectionInterfaceState.theme.rootController.navigationBar.primaryTextColor) - self.setNeedsLayout() - } - - if mediaCollectionInterfaceState.selectingMode != self.mediaCollectionInterfaceState.selectingMode { - let previousSelectingMode = self.mediaCollectionInterfaceState.selectingMode - let arrowTransform = CATransform3DMakeScale(1.0, previousSelectingMode ? 1.0 : -1.0, 1.0) - if animated { - self.arrowView.layer.animate(from: NSNumber(value: Float(previousSelectingMode ? -1.0 : 1.0)), to: NSNumber(value: Float(previousSelectingMode ? 1.0 : -1.0)), keyPath: "transform.scale.y", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3) - } - self.arrowView.layer.transform = arrowTransform - } - self.mediaCollectionInterfaceState = mediaCollectionInterfaceState - } - } - - @objc func buttonPressed() { - self.toggle() - } -} diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift index 185ef751dc..07046ff1f5 100644 --- a/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -210,7 +210,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { case let .index(index): switch self.location { case let .messages(peerId, tagMask, _): - self.navigationDisposable.set((self.postbox.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 10, anchorIndex: index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] view in + self.navigationDisposable.set((self.postbox.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 10, clipHoles: false, anchorIndex: index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] view in if let strongSelf = self { assert(strongSelf.loadingItem) diff --git a/TelegramUI/PeerNotificationSoundStrings.swift b/TelegramUI/PeerNotificationSoundStrings.swift index d13ca03ad6..8a14daeb47 100644 --- a/TelegramUI/PeerNotificationSoundStrings.swift +++ b/TelegramUI/PeerNotificationSoundStrings.swift @@ -27,10 +27,12 @@ private let classicSounds: [String] = [ "Telegraph" ] -func localizedPeerNotificationSoundString(strings: PresentationStrings, sound: PeerMessageSound) -> String { +private func soundName(strings: PresentationStrings, sound: PeerMessageSound) -> String { switch sound { case .none: - return strings.Settings_UsernameEmpty + return "None" + case .default: + return "" case let .bundledModern(id): if id >= 0 && Int(id) < modernSounds.count { return modernSounds[Int(id)] @@ -43,3 +45,23 @@ func localizedPeerNotificationSoundString(strings: PresentationStrings, sound: P return "Sound \(id)" } } + +func localizedPeerNotificationSoundString(strings: PresentationStrings, sound: PeerMessageSound, default: PeerMessageSound? = nil) -> String { + switch sound { + case .default: + if let defaultSound = `default` { + let name = soundName(strings: strings, sound: defaultSound) + let actualName: String + if name.isEmpty { + actualName = soundName(strings: strings, sound: .bundledModern(id: 0)) + } else { + actualName = name + } + return "Default (\(actualName))" + } else { + return "Default" + } + default: + return soundName(strings: strings, sound: sound) + } +} diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 10a8db1435..9a01652b71 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -134,8 +134,8 @@ public final class PeerSelectionController: ViewController { private func deactivateSearch() { if !self.displayNavigationBar { - self.peerSelectionNode.deactivateSearch() self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.peerSelectionNode.deactivateSearch() } } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 7767f633a9..65b3d10fdf 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -77,47 +77,52 @@ private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, } private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pathExtension: String? = nil, progressive: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let thumbnailResource = smallestRepresentation.resource - let fullSizeResource = file.resource - - let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension) - - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in - if maybeData.complete { - return .single((nil, maybeData.path, true)) + let thumbnailResource = smallestImageRepresentation(file.previewRepresentations)?.resource + let fullSizeResource = file.resource + + let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension) + + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in + if maybeData.complete { + return .single((nil, maybeData.path, true)) + } else { + let fetchedThumbnail: Signal + if let thumbnailResource = thumbnailResource { + fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) - - let thumbnail = Signal { subscriber in + fetchedThumbnail = .complete() + } + + let thumbnail: Signal + if let thumbnailResource = thumbnailResource { + thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) - }, error: subscriber.putError, completed: subscriber.putCompletion) + }, error: subscriber.putError, completed: subscriber.putCompletion) return ActionDisposable { fetchedDisposable.dispose() thumbnailDisposable.dispose() } } - - - let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false)) |> map { next -> (String?, Bool) in - return (next.size == 0 ? nil : next.path, next.complete) - } - - return thumbnail |> mapToSignal { thumbnailData in - return fullSizeDataAndPath |> map { (dataPath, complete) in - return (thumbnailData, dataPath, complete) - } + } else { + thumbnail = .single(nil) + } + + let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false)) |> map { next -> (String?, Bool) in + return (next.size == 0 ? nil : next.path, next.complete) + } + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeDataAndPath |> map { (dataPath, complete) in + return (thumbnailData, dataPath, complete) } } - } |> filter({ $0.0 != nil || $0.1 != nil }) - - return signal - } else { - return .never() - } + } + } |> filter({ $0.0 != nil || $0.1 != nil }) + + return signal } private func chatMessageVideoDatas(account: Account, file: TelegramMediaFile) -> Signal<(Data?, (Data, String)?, Bool), NoError> { @@ -511,8 +516,31 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) - c.fill(arguments.drawingRect) + + let blurSourceImage = thumbnailImage ?? fullSizeImage + + if let fullSizeImage = blurSourceImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 154.0, height: 154.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + //telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: (arguments.drawingRect.width - filledSize.width) / 2.0, y: (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(UIColor(white: 1.0, alpha: 0.6).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { + c.fill(arguments.drawingRect) + } } c.setBlendMode(.copy) @@ -983,7 +1011,27 @@ func mediaGridMessageVideo(account: Account, video: TelegramMediaFile) -> Signal context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { - c.fill(arguments.drawingRect) + if let fullSizeImage = fullSizeImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.draw(blurredImage.cgImage!, in: CGRect(origin: CGPoint(x: (arguments.drawingRect.width - filledSize.width) / 2.0, y: (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(UIColor(white: 1.0, alpha: 0.1).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { + c.fill(arguments.drawingRect) + } } c.setBlendMode(.copy) diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index b7662fafd0..9f0eb1a5a7 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -13,7 +13,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { } public struct ApplicationSpecificPreferencesKeys { - static let inAppNotificationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.inAppNotificationSettings.rawValue) + public static let inAppNotificationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.inAppNotificationSettings.rawValue) public static let presentationPasscodeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.presentationPasscodeSettings.rawValue) public static let automaticMediaDownloadSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.automaticMediaDownloadSettings.rawValue) public static let generatedMediaStoreSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.generatedMediaStoreSettings.rawValue) diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 1a2bd3f780..a98c8bce15 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,7 +4,7 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, readStateData: ChatHistoryCombinedInitialReadStateData?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: ChatHistoryCombinedInitialReadStateData?) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -183,7 +183,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } } - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, readStateData: readStateData, scrolledToIndex: scrolledToIndex)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, scrolledToIndex: scrolledToIndex)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 72cae11e18..ebf8010638 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -28,13 +28,13 @@ enum PresentationResourceKey: Int32 { case navigationPlayerMaximizedRepeatIcon case navigationPlayerHandleIcon - case navigationDropdownArrowImage - case itemListDisclosureArrow case itemListCheckIcon + case itemListSecondaryCheckIcon case itemListPlusIcon case itemListStickerItemUnreadDot + case itemListVerifiedPeerIcon case chatListLockTopLockedImage case chatListLockBottomLockedImage @@ -45,6 +45,9 @@ enum PresentationResourceKey: Int32 { case chatListBadgeBackgroundActive case chatListBadgeBackgroundInactive case chatListBadgeBackgroundMention + case chatListBadgeBackgroundPinned + case chatListMutedIcon + case chatListVerifiedIcon case chatPrincipalThemeEssentialGraphics case chatBubbleVerticalLineIncomingImage @@ -65,6 +68,7 @@ enum PresentationResourceKey: Int32 { case chatBubbleMediaOverlayControlSecret + case chatLoadingIndicatorBackgroundImage case chatServiceBubbleFillImage case chatBubbleSecretMediaIcon @@ -143,4 +147,7 @@ enum PresentationResourceKey: Int32 { case callListOutgoingIcon case callListInfoButton + + case genericSearchBarLoupeImage + case genericSearchBar } diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index e2fdf9bccc..2461dc234b 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -551,7 +551,7 @@ struct PresentationResourcesChat { 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) + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/RevealActionUnmuteIcon"), color: theme.chat.inputPanel.panelControlAccentColor) }) } @@ -569,25 +569,31 @@ struct PresentationResourcesChat { 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) + return generateStretchableFilledCircleImage(diameter: 9.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) + return generateStretchableFilledCircleImage(diameter: 9.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) + return generateStretchableFilledCircleImage(diameter: 9.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) + return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.bubble.outgoingAccentColor) + }) + } + + static func chatLoadingIndicatorBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatLoadingIndicatorBackgroundImage.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 30.0, color: theme.chat.serviceMessage.serviceMessageFillColor) }) } } diff --git a/TelegramUI/PresentationResourcesChatList.swift b/TelegramUI/PresentationResourcesChatList.swift index 80e1fe9990..8025a16bc4 100644 --- a/TelegramUI/PresentationResourcesChatList.swift +++ b/TelegramUI/PresentationResourcesChatList.swift @@ -91,4 +91,22 @@ struct PresentationResourcesChatList { return generateBadgeBackgroundImage(theme: theme, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: .white)) }) } + + static func badgeBackgroundPinned(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListBadgeBackgroundPinned.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPinnedIcon"), color: theme.chatList.pinnedBadgeColor) + }) + } + + static func mutedIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListMutedIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerMutedIcon"), color: theme.chatList.muteIconColor) + }) + } + + static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in + return UIImage(bundleImageName: "Chat List/PeerVerifiedIcon")?.precomposed() + }) + } } diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index d42fb52708..9cadd2cb86 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -10,10 +10,10 @@ private func generateArrowImage(_ theme: PresentationTheme) -> UIImage? { }) } -private func generateCheckIcon(_ theme: PresentationTheme) -> UIImage? { +private func generateCheckIcon(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.list.itemAccentColor.cgColor) + context.setStrokeColor(color.cgColor) context.setLineWidth(2.0) context.move(to: CGPoint(x: 12.0, y: 1.0)) context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) @@ -38,7 +38,15 @@ struct PresentationResourcesItemList { } static func checkIconImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.itemListCheckIcon.rawValue, generateCheckIcon) + return theme.image(PresentationResourceKey.itemListCheckIcon.rawValue, { theme in + return generateCheckIcon(color: theme.list.itemAccentColor) + }) + } + + static func secondaryCheckIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListSecondaryCheckIcon.rawValue, { theme in + return generateCheckIcon(color: theme.list.itemSecondaryTextColor) + }) } static func plusIconImage(_ theme: PresentationTheme) -> UIImage? { @@ -50,4 +58,10 @@ struct PresentationResourcesItemList { return generateFilledCircleImage(diameter: 6.0, color: theme.list.itemAccentColor) }) } + + static func verifiedPeerIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListVerifiedPeerIcon.rawValue, { theme in + return UIImage(bundleImageName: "Item List/PeerVerifiedIcon")?.precomposed() + }) + } } diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index c374b0e85b..4483eb15d2 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -1,20 +1,12 @@ import Foundation import Display -private func generateComposeButtonImage(theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) - try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ") - }) -} - 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 +func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), 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 ") @@ -65,19 +57,15 @@ struct PresentationResourcesRootController { } static func navigationComposeIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.navigationComposeIcon.rawValue, generateComposeButtonImage) + return theme.image(PresentationResourceKey.navigationComposeIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/ComposeIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) } static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) } - static func navigationDropdownArrowImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.navigationDropdownArrowImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/TitleViewModeSelectionArrow"), color: theme.rootController.navigationBar.accentTextColor) - }) - } - static func navigationCallIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationCallIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.rootController.navigationBar.accentTextColor) @@ -103,13 +91,13 @@ struct PresentationResourcesRootController { static func navigationPlayerPlayIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationPlayerPlayIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.rootController.navigationBar.primaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.rootController.navigationBar.accentTextColor) }) } static func navigationPlayerPauseIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationPlayerPauseIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.rootController.navigationBar.primaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.rootController.navigationBar.accentTextColor) }) } diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index ba1cfc93ba..fe1935ccf3 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -96,6 +96,7 @@ public final class PresentationStrings { private let lc: UInt32 public let languageCode: String + public let dict: [String: String] public let Channel_BanUser_Title: String public let Notification_SecretChatMessageScreenshotSelf: String @@ -4989,6 +4990,7 @@ public final class PresentationStrings { init(languageCode: String, dict: [String: String]) { self.languageCode = languageCode + self.dict = dict var rawCode = languageCode as NSString let range = rawCode.range(of: "_") if range.location != NSNotFound { diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index a0c2d8fc74..20d8fbfaf1 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -149,15 +149,17 @@ public final class PresentationThemeActiveNavigationSearchBar { public let inputTextColor: UIColor public let inputPlaceholderTextColor: UIColor public let inputIconColor: UIColor + public let inputClearButtonColor: UIColor public let separatorColor: UIColor - public init(backgroundColor: UIColor, accentColor: UIColor, inputFillColor: UIColor, inputTextColor: UIColor, inputPlaceholderTextColor: UIColor, inputIconColor: UIColor, separatorColor: UIColor) { + public init(backgroundColor: UIColor, accentColor: UIColor, inputFillColor: UIColor, inputTextColor: UIColor, inputPlaceholderTextColor: UIColor, inputIconColor: UIColor, inputClearButtonColor: UIColor, separatorColor: UIColor) { self.backgroundColor = backgroundColor self.accentColor = accentColor self.inputFillColor = inputFillColor self.inputTextColor = inputTextColor self.inputPlaceholderTextColor = inputPlaceholderTextColor self.inputIconColor = inputIconColor + self.inputClearButtonColor = inputClearButtonColor self.separatorColor = separatorColor } @@ -168,6 +170,7 @@ public final class PresentationThemeActiveNavigationSearchBar { self.inputTextColor = try parseColor(decoder, "inputTextColor") self.inputPlaceholderTextColor = try parseColor(decoder, "inputPlaceholderTextColor") self.inputIconColor = try parseColor(decoder, "inputIconColor") + self.inputClearButtonColor = try parseColor(decoder, "inputClearButtonColor") self.separatorColor = try parseColor(decoder, "separatorColor") } @@ -350,17 +353,19 @@ public final class PresentationThemeChatList { public let messageDraftTextColor: UIColor public let checkmarkColor: UIColor public let pendingIndicatorColor: UIColor + public let muteIconColor: UIColor public let unreadBadgeActiveBackgroundColor: UIColor public let unreadBadgeActiveTextColor: UIColor public let unreadBadgeInactiveBackgroundColor: UIColor public let unreadBadgeInactiveTextColor: UIColor + public let pinnedBadgeColor: UIColor public let pinnedSearchBarColor: UIColor public let regularSearchBarColor: UIColor public let sectionHeaderFillColor: UIColor public let sectionHeaderTextColor: UIColor public let searchBarKeyboardColor: PresentationThemeKeyboardColor - init(backgroundColor: UIColor, itemSeparatorColor: UIColor, itemBackgroundColor: UIColor, pinnedItemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, titleColor: UIColor, secretTitleColor: UIColor, dateTextColor: UIColor, authorNameColor: UIColor, messageTextColor: UIColor, messageDraftTextColor: UIColor, checkmarkColor: UIColor, pendingIndicatorColor: UIColor, unreadBadgeActiveBackgroundColor: UIColor, unreadBadgeActiveTextColor: UIColor, unreadBadgeInactiveBackgroundColor: UIColor, unreadBadgeInactiveTextColor: UIColor, pinnedSearchBarColor: UIColor, regularSearchBarColor: UIColor, sectionHeaderFillColor: UIColor, sectionHeaderTextColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) { + init(backgroundColor: UIColor, itemSeparatorColor: UIColor, itemBackgroundColor: UIColor, pinnedItemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, titleColor: UIColor, secretTitleColor: UIColor, dateTextColor: UIColor, authorNameColor: UIColor, messageTextColor: UIColor, messageDraftTextColor: UIColor, checkmarkColor: UIColor, pendingIndicatorColor: UIColor, muteIconColor: UIColor, unreadBadgeActiveBackgroundColor: UIColor, unreadBadgeActiveTextColor: UIColor, unreadBadgeInactiveBackgroundColor: UIColor, unreadBadgeInactiveTextColor: UIColor, pinnedBadgeColor: UIColor, pinnedSearchBarColor: UIColor, regularSearchBarColor: UIColor, sectionHeaderFillColor: UIColor, sectionHeaderTextColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) { self.backgroundColor = backgroundColor self.itemSeparatorColor = itemSeparatorColor self.itemBackgroundColor = itemBackgroundColor @@ -374,10 +379,12 @@ public final class PresentationThemeChatList { self.messageDraftTextColor = messageDraftTextColor self.checkmarkColor = checkmarkColor self.pendingIndicatorColor = pendingIndicatorColor + self.muteIconColor = muteIconColor self.unreadBadgeActiveBackgroundColor = unreadBadgeActiveBackgroundColor self.unreadBadgeActiveTextColor = unreadBadgeActiveTextColor self.unreadBadgeInactiveBackgroundColor = unreadBadgeInactiveBackgroundColor self.unreadBadgeInactiveTextColor = unreadBadgeInactiveTextColor + self.pinnedBadgeColor = pinnedBadgeColor self.pinnedSearchBarColor = pinnedSearchBarColor self.regularSearchBarColor = regularSearchBarColor self.sectionHeaderFillColor = sectionHeaderFillColor @@ -399,10 +406,12 @@ public final class PresentationThemeChatList { self.messageDraftTextColor = try parseColor(decoder, "messageDraftTextColor") self.checkmarkColor = try parseColor(decoder, "checkmarkColor") self.pendingIndicatorColor = try parseColor(decoder, "pendingIndicatorColor") + self.muteIconColor = try parseColor(decoder, "muteIconColor") self.unreadBadgeActiveBackgroundColor = try parseColor(decoder, "unreadBadgeActiveBackgroundColor") self.unreadBadgeActiveTextColor = try parseColor(decoder, "unreadBadgeActiveTextColor") self.unreadBadgeInactiveBackgroundColor = try parseColor(decoder, "unreadBadgeInactiveBackgroundColor") self.unreadBadgeInactiveTextColor = try parseColor(decoder, "unreadBadgeInactiveTextColor") + self.pinnedBadgeColor = try parseColor(decoder, "pinnedBadgeColor") self.pinnedSearchBarColor = try parseColor(decoder, "pinnedSearchBarColor") self.regularSearchBarColor = try parseColor(decoder, "regularSearchBarColor") self.sectionHeaderFillColor = try parseColor(decoder, "sectionHeaderFillColor") @@ -602,6 +611,7 @@ public final class PresentationThemeChatBubble { public final class PresentationThemeServiceMessage { public let serviceMessageFillColor: UIColor public let serviceMessagePrimaryTextColor: UIColor + public let serviceMessageLinkHighlightColor: UIColor public let unreadBarFillColor: UIColor public let unreadBarStrokeColor: UIColor @@ -611,9 +621,10 @@ public final class PresentationThemeServiceMessage { public let dateFillFloatingColor: UIColor public let dateTextColor: UIColor - public init(serviceMessageFillColor: UIColor, serviceMessagePrimaryTextColor: UIColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateFillStaticColor: UIColor, dateFillFloatingColor: UIColor, dateTextColor: UIColor) { + public init(serviceMessageFillColor: UIColor, serviceMessagePrimaryTextColor: UIColor, serviceMessageLinkHighlightColor: UIColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateFillStaticColor: UIColor, dateFillFloatingColor: UIColor, dateTextColor: UIColor) { self.serviceMessageFillColor = serviceMessageFillColor self.serviceMessagePrimaryTextColor = serviceMessagePrimaryTextColor + self.serviceMessageLinkHighlightColor = serviceMessageLinkHighlightColor self.unreadBarFillColor = unreadBarFillColor self.unreadBarStrokeColor = unreadBarStrokeColor self.unreadBarTextColor = unreadBarTextColor @@ -625,6 +636,7 @@ public final class PresentationThemeServiceMessage { public init(decoder: PostboxDecoder) throws { self.serviceMessageFillColor = try parseColor(decoder, "serviceMessageFillColor") self.serviceMessagePrimaryTextColor = try parseColor(decoder, "serviceMessagePrimaryTextColor") + self.serviceMessageLinkHighlightColor = try parseColor(decoder, "serviceMessageLinkHighlightColor") self.unreadBarFillColor = try parseColor(decoder, "unreadBarFillColor") self.unreadBarStrokeColor = try parseColor(decoder, "unreadBarStrokeColor") self.unreadBarTextColor = try parseColor(decoder, "unreadBarTextColor") diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 757a0eb769..7046b99c9f 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -239,54 +239,42 @@ private struct PrivacyAndSecurityControllerState: Equatable { } } -private func stringForSelectiveSettings(_ settings: SelectivePrivacySettings) -> String { +private func stringForSelectiveSettings(strings: PresentationStrings, settings: SelectivePrivacySettings) -> String { switch settings { case let .disableEveryone(enableFor): if enableFor.isEmpty { - return "Nobody" + return strings.PrivacySettings_LastSeenNobody } else { - return "Nobody (+\(enableFor.count))" + return strings.PrivacySettings_LastSeenNobodyPlus("\(enableFor.count)").0 } case let .enableEveryone(disableFor): if disableFor.isEmpty { - return "Everybody" + return strings.PrivacySettings_LastSeenEverybody } else { - return "Everybody (-\(disableFor.count))" + return strings.PrivacySettings_LastSeenEverybodyMinus("\(disableFor.count)").0 } case let .enableContacts(enableFor, disableFor): if !enableFor.isEmpty && !disableFor.isEmpty { - return "My Contacts (+\(enableFor.count), -\(disableFor.count))" + return strings.PrivacySettings_LastSeenContactsMinusPlus("\(enableFor.count)", "\(disableFor.count)").0 } else if !enableFor.isEmpty { - return "My Contacts (+\(enableFor.count))" + return strings.PrivacySettings_LastSeenContactsPlus("\(enableFor.count)").0 } else if !disableFor.isEmpty { - return "My Contacts (-\(disableFor.count))" + return strings.PrivacySettings_LastSeenContactsMinus("\(enableFor.count)").0 } else { - return "My Contacts" + return strings.PrivacySettings_LastSeenContacts } } } -private func stringForAccountTimeout(_ timeout: Int32) -> String { - if timeout <= 1 * 31 * 24 * 60 * 60 { - return "1 month" - } else if timeout <= 3 * 31 * 24 * 60 * 60 { - return "3 months" - } else if timeout <= 6 * 31 * 24 * 60 * 60 { - return "6 months" - } else { - return "1 year" - } -} - private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers)) if let privacySettings = privacySettings { - entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, stringForSelectiveSettings(privacySettings.presence))) - entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(privacySettings.groupInvitations))) - entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, stringForSelectiveSettings(privacySettings.voiceCalls))) + entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.presence))) + entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations))) + entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceCalls))) } else { entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading)) entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) @@ -305,7 +293,7 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD } else { value = privacySettings.accountRemovalTimeout } - entries.append(.accountTimeout(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountIfAwayFor, stringForAccountTimeout(value))) + entries.append(.accountTimeout(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountIfAwayFor, timeIntervalString(strings: presentationData.strings, value: value))) } else { entries.append(.accountTimeout(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountIfAwayFor, presentationData.strings.Channel_NotificationLoading)) } @@ -429,6 +417,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign |> deliverOnMainQueue updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in if let _ = privacySettingsValue { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -457,26 +446,21 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign })) } } + let timeoutValues: [Int32] = [ + 1 * 30 * 24 * 60 * 60, + 3 * 30 * 24 * 60 * 60, + 6 * 30 * 24 * 60 * 60, + 12 * 30 * 24 * 60 * 60 + ] + let timeoutItems: [ActionSheetItem] = timeoutValues.map { value in + return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value), action: { + dismissAction() + timeoutAction(value) + }) + } controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "1 month", action: { - dismissAction() - timeoutAction(1 * 30 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "3 months", action: { - dismissAction() - timeoutAction(3 * 30 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "6 months", action: { - dismissAction() - timeoutAction(6 * 30 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "1 year", action: { - dismissAction() - timeoutAction(12 * 30 * 24 * 60 * 60) - }), - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: timeoutItems), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) } @@ -491,7 +475,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Privacy and Security"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift new file mode 100644 index 0000000000..8e1d8fd7f3 --- /dev/null +++ b/TelegramUI/SaveToCameraRoll.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import Photos + +func saveToCameraRoll(postbox: Postbox, media: Media) -> Signal { + var resource: MediaResource? + var isImage = false + if let image = media as? TelegramMediaImage { + if let representation = largestImageRepresentation(image.representations) { + resource = representation.resource + isImage = true + } + } else if let file = media as? TelegramMediaFile { + resource = file.resource + } + + if let resource = resource { + let fetchedData: Signal = Signal { subscriber in + let fetched = postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let data = postbox.mediaBox.resourceData(resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: true)).start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + }) + return ActionDisposable { + fetched.dispose() + data.dispose() + } + } + return fetchedData + |> mapToSignal { data -> Signal in + if data.complete { + return Signal { subscriber in + let tempVideoPath = NSTemporaryDirectory() + "\(arc4random64()).mp4" + PHPhotoLibrary.shared().performChanges({ + if isImage { + if let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: data) { + PHAssetChangeRequest.creationRequestForAsset(from: image) + } + } else { + if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempVideoPath) { + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath)) + } + } + }, completionHandler: { _, error in + if let error = error { + print("\(error)") + } + let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) + subscriber.putNext(Void()) + subscriber.putCompletion() + }) + + return ActionDisposable { + } + } + } else { + return .complete() + } + } |> take(1) |> mapToSignal { _ -> Signal in return .complete() } + } else { + return .complete() + } +} diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index a35d2b91f8..d50c817337 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -4,8 +4,16 @@ import UIKit import AsyncDisplayKit import Display +private func generateLoupeIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color) +} + +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - let diameter: CGFloat = 8.0 + let diameter: CGFloat = 10.0 return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -15,16 +23,17 @@ private func generateBackground(backgroundColor: UIColor, foregroundColor: UICol } private class SearchBarTextField: UITextField { - fileprivate let placeholderLabel: UILabel - private var placeholderLabelConstrainedSize: CGSize? - private var placeholderLabelSize: CGSize? + let placeholderLabel: TextNode + var placeholderString: NSAttributedString? override init(frame: CGRect) { - self.placeholderLabel = UILabel() + self.placeholderLabel = TextNode() + self.placeholderLabel.isLayerBacked = true + self.placeholderLabel.displaysAsynchronously = false super.init(frame: frame) - self.addSubview(self.placeholderLabel) + self.addSubnode(self.placeholderLabel) } required init?(coder aDecoder: NSCoder) { @@ -42,14 +51,14 @@ private class SearchBarTextField: UITextField { override func layoutSubviews() { super.layoutSubviews() - let constrainedSize = self.textRect(forBounds: self.bounds).size - if self.placeholderLabelConstrainedSize != constrainedSize { - self.placeholderLabelConstrainedSize = constrainedSize - self.placeholderLabelSize = self.placeholderLabel.sizeThatFits(constrainedSize) - } + let bounds = self.bounds - if let placeholderLabelSize = self.placeholderLabelSize { - self.placeholderLabel.frame = CGRect(origin: self.textRect(forBounds: self.bounds).origin, size: placeholderLabelSize) + let constrainedSize = self.textRect(forBounds: self.bounds).size + if let placeholderString = self.placeholderString { + let makeLayout = TextNode.asyncLayout(self.placeholderLabel) + let (labelLayout, labelApply) = makeLayout(placeholderString, nil, 1, .end, constrainedSize, .left, nil, UIEdgeInsets()) + let _ = labelApply() + self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + UIScreenPixel), size: labelLayout.size) } } } @@ -61,14 +70,16 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let textBackgroundNode: ASImageNode + private let iconNode: ASImageNode private let textField: SearchBarTextField + private let clearButton: HighlightableButtonNode private let cancelButton: ASButtonNode var placeholderString: NSAttributedString? { get { - return self.textField.placeholderLabel.attributedText + return self.textField.placeholderString } set(value) { - self.textField.placeholderLabel.attributedText = value + self.textField.placeholderString = value } } @@ -93,12 +104,25 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textBackgroundNode.displayWithoutProcessing = true self.textBackgroundNode.image = generateBackground(backgroundColor: theme.rootController.activeNavigationSearchBar.backgroundColor, foregroundColor: theme.rootController.activeNavigationSearchBar.inputFillColor) + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.image = generateLoupeIcon(color: theme.rootController.activeNavigationSearchBar.inputIconColor) + self.textField = SearchBarTextField() self.textField.autocorrectionType = .no self.textField.returnKeyType = .done - self.textField.font = Font.regular(15.0) + self.textField.font = Font.regular(14.0) self.textField.textColor = theme.rootController.activeNavigationSearchBar.inputTextColor + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateClearIcon(color: theme.rootController.activeNavigationSearchBar.inputClearButtonColor), for: []) + self.clearButton.isHidden = true + switch theme.chatList.searchBarKeyboardColor { case .light: self.textField.keyboardAppearance = .default @@ -118,12 +142,15 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.addSubnode(self.textBackgroundNode) self.view.addSubview(self.textField) + self.addSubnode(self.iconNode) + self.addSubnode(self.clearButton) self.addSubnode(self.cancelButton) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { @@ -146,9 +173,20 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) self.cancelButton.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - 8.0 - cancelButtonSize.width, y: 20.0 + 10.0), size: cancelButtonSize) - self.textBackgroundNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 20.0 + 8.0), size: CGSize(width: self.bounds.size.width - 16.0 - cancelButtonSize.width - 10.0, height: 28.0)) + let textBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 20.0 + 8.0), size: CGSize(width: self.bounds.size.width - 16.0 - cancelButtonSize.width - 11.0, height: 28.0)) + self.textBackgroundNode.frame = textBackgroundFrame - self.textField.frame = self.textBackgroundNode.frame + let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 23.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 23.0 - 20.0), height: textBackgroundFrame.size.height)) + + if let iconImage = self.iconNode.image { + let iconSize = iconImage.size + self.iconNode.frame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 8.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize) + } + + let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0)) + self.clearButton.frame = CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize) + + self.textField.frame = textFrame } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @@ -180,6 +218,10 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { let initialLabelNodeFrame = CGRect(origin: node.labelNode.frame.offsetBy(dx: initialTextBackgroundFrame.origin.x - 4.0, dy: initialTextBackgroundFrame.origin.y - 6.0).origin, size: textFieldFrame.size) self.textField.layer.animateFrame(from: initialLabelNodeFrame, to: self.textField.frame, duration: duration, timingFunction: timingFunction) + let iconFrame = self.iconNode.frame + let initialIconFrame = CGRect(origin: node.iconNode.frame.offsetBy(dx: initialTextBackgroundFrame.origin.x, dy: initialTextBackgroundFrame.origin.y).origin, size: iconFrame.size) + self.iconNode.layer.animateFrame(from: initialIconFrame, to: self.iconNode.frame, duration: duration, timingFunction: timingFunction) + let cancelButtonFrame = self.cancelButton.frame self.cancelButton.layer.animatePosition(from: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: initialTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), to: self.cancelButton.layer.position, duration: duration, timingFunction: timingFunction) node.isHidden = true @@ -193,9 +235,64 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } } - func transitionOut(to node: SearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: () -> Void) { - node.isHidden = false - completion() + func transitionOut(to node: SearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + let targetTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) + + let duration: Double = 0.5 + let timingFunction = kCAMediaTimingFunctionSpring + + node.isHidden = true + self.clearButton.isHidden = true + self.textField.text = "" + + var backgroundCompleted = false + var separatorCompleted = false + var textBackgroundCompleted = false + let intermediateCompletion: () -> Void = { [weak node] in + if backgroundCompleted && separatorCompleted && textBackgroundCompleted { + completion() + node?.isHidden = false + } + } + + let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0))) + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + backgroundCompleted = true + intermediateCompletion() + }) + + let targetSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, targetTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) + self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + self.separatorNode.layer.animateFrame(from: self.separatorNode.frame, to: targetSeparatorFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + separatorCompleted = true + intermediateCompletion() + }) + + self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + textBackgroundCompleted = true + intermediateCompletion() + }) + + let transitionBackgroundNode = ASImageNode() + transitionBackgroundNode.isLayerBacked = true + transitionBackgroundNode.displaysAsynchronously = false + transitionBackgroundNode.displayWithoutProcessing = true + transitionBackgroundNode.image = node.backgroundNode.image + self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode) + transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false) + transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + let textFieldFrame = self.textField.frame + let targetLabelNodeFrame = CGRect(origin: node.labelNode.frame.offsetBy(dx: targetTextBackgroundFrame.origin.x - 4.0, dy: targetTextBackgroundFrame.origin.y - 5.0 + UIScreenPixel).origin, size: textFieldFrame.size) + self.textField.layer.animateFrame(from: self.textField.frame, to: targetLabelNodeFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + let iconFrame = self.iconNode.frame + let targetIconFrame = CGRect(origin: node.iconNode.frame.offsetBy(dx: targetTextBackgroundFrame.origin.x, dy: targetTextBackgroundFrame.origin.y).origin, size: iconFrame.size) + self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + let cancelButtonFrame = self.cancelButton.frame + self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @@ -211,7 +308,11 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } @objc func textFieldDidChange(_ textField: UITextField) { - self.textField.placeholderLabel.isHidden = !(textField.text?.isEmpty ?? true) + let isEmpty = !(textField.text?.isEmpty ?? true) + if isEmpty != self.textField.placeholderLabel.isHidden { + self.textField.placeholderLabel.isHidden = isEmpty + self.clearButton.isHidden = !isEmpty + } if let textUpdated = self.textUpdated { textUpdated(textField.text ?? "") } @@ -222,4 +323,9 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { cancel() } } + + @objc func clearPressed() { + self.textField.text = "" + self.textFieldDidChange(self.textField) + } } diff --git a/TelegramUI/SearchBarPlaceholderNode.swift b/TelegramUI/SearchBarPlaceholderNode.swift index 5e6bf47bae..2b0ef3ec18 100644 --- a/TelegramUI/SearchBarPlaceholderNode.swift +++ b/TelegramUI/SearchBarPlaceholderNode.swift @@ -4,8 +4,14 @@ import UIKit import AsyncDisplayKit import Display +private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe") + +private func generateLoupeIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: templateLoupeIcon, color: color) +} + private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - let diameter: CGFloat = 8.0 + let diameter: CGFloat = 10.0 return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -27,10 +33,12 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { var activate: (() -> Void)? let backgroundNode: ASImageNode - var foregroundColor: UIColor + private var foregroundColor: UIColor + private var iconColor: UIColor + let iconNode: ASImageNode let labelNode: TextNode - var placeholderString: NSAttributedString? + private(set) var placeholderString: NSAttributedString? override init() { self.backgroundNode = ASImageNode() @@ -39,9 +47,15 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { self.backgroundNode.displayWithoutProcessing = true self.foregroundColor = UIColor(rgb: 0xededed) + self.iconColor = UIColor(rgb: 0x000000, alpha: 0.0) self.backgroundNode.image = generateBackground(backgroundColor: UIColor.white, foregroundColor: self.foregroundColor) + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.labelNode = TextNode() self.labelNode.isOpaque = true self.labelNode.isLayerBacked = true @@ -50,6 +64,7 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { super.init() self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) self.addSubnode(self.labelNode) self.backgroundNode.isUserInteractionEnabled = true @@ -61,31 +76,45 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { self.backgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTap(_:)))) } - func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ foregoundColor: UIColor, _ backgroundColor: UIColor) -> (() -> Void) { + func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor) -> (() -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let currentForegroundColor = self.foregroundColor + let currentIconColor = self.iconColor - return { placeholderString, constrainedSize, foregroundColor, backgroundColor in + return { placeholderString, constrainedSize, iconColor, foregroundColor, backgroundColor in let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) var updatedBackgroundImage: UIImage? + var updatedIconImage: UIImage? if !currentForegroundColor.isEqual(foregroundColor) { updatedBackgroundImage = generateBackground(backgroundColor: backgroundColor, foregroundColor: foregroundColor) } + if !currentIconColor.isEqual(iconColor) { + updatedIconImage = generateLoupeIcon(color: iconColor) + } return { [weak self] in if let strongSelf = self { let _ = labelApply() strongSelf.foregroundColor = foregroundColor + strongSelf.iconColor = iconColor if let updatedBackgroundImage = updatedBackgroundImage { strongSelf.backgroundNode.image = updatedBackgroundImage strongSelf.labelNode.backgroundColor = foregroundColor } + if let updatedIconImage = updatedIconImage { + strongSelf.iconNode.image = updatedIconImage + } strongSelf.placeholderString = placeholderString - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - labelLayoutResult.size.width) / 2.0), y: floor((28.0 - labelLayoutResult.size.height) / 2.0) + 2.0), size: labelLayoutResult.size) + let labelFrame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - labelLayoutResult.size.width) / 2.0), y: floor((28.0 - labelLayoutResult.size.height) / 2.0) - UIScreenPixel), size: labelLayoutResult.size) + strongSelf.labelNode.frame = labelFrame + if let iconImage = strongSelf.iconNode.image { + let iconSize = iconImage.size + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: labelFrame.minX - 4.0 - iconSize.width, y: floor((28.0 - iconSize.height) / 2.0) + UIScreenPixel), size: iconSize) + } strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: 28.0)) } } diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 64997db45f..91faa38389 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -5,10 +5,12 @@ import Display final class SearchDisplayController { private let searchBar: SearchBarNode - private let contentNode: SearchDisplayControllerContentNode + let contentNode: SearchDisplayControllerContentNode private var containerLayout: (ContainerViewLayout, CGFloat)? + private(set) var isDeactivating = false + init(theme: PresentationTheme, strings: PresentationStrings, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { self.searchBar = SearchBarNode(theme: theme, strings: strings) self.contentNode = contentNode @@ -16,7 +18,17 @@ final class SearchDisplayController { self.searchBar.textUpdated = { [weak contentNode] text in contentNode?.searchTextUpdated(text: text) } - self.searchBar.cancel = cancel + self.searchBar.cancel = { [weak self] in + self?.isDeactivating = true + cancel() + } + self.contentNode.cancel = { [weak self] in + self?.isDeactivating = true + cancel() + } + self.contentNode.dismissInput = { [weak self] in + self?.searchBar.deactivate(clear: false) + } } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { @@ -71,6 +83,12 @@ final class SearchDisplayController { let contentNode = self.contentNode if animated { + if let placeholder = placeholder, let (_, navigationBarHeight) = self.containerLayout { + let contentNodePosition = self.contentNode.layer.position + let targetTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: self.contentNode.supernode) + + self.contentNode.layer.animatePosition(from: contentNodePosition, to: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (targetTextBackgroundFrame.maxY + 8.0 - navigationBarHeight)), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in contentNode?.removeFromSupernode() }) diff --git a/TelegramUI/SearchDisplayControllerContentNode.swift b/TelegramUI/SearchDisplayControllerContentNode.swift index 8bf4827485..9a9a43c468 100644 --- a/TelegramUI/SearchDisplayControllerContentNode.swift +++ b/TelegramUI/SearchDisplayControllerContentNode.swift @@ -4,6 +4,9 @@ import Display import SwiftSignalKit class SearchDisplayControllerContentNode: ASDisplayNode { + final var dismissInput: (() -> Void)? + final var cancel: (() -> Void)? + override init() { super.init() diff --git a/TelegramUI/SelectablePeerNode.swift b/TelegramUI/SelectablePeerNode.swift new file mode 100644 index 0000000000..a25f0a396f --- /dev/null +++ b/TelegramUI/SelectablePeerNode.swift @@ -0,0 +1,148 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +import LegacyComponents + +private let selectionBackgroundImage = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0x007ee5).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) +}) + +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 24.0)! +private let textFont = Font.regular(11.0) + +final class SelectablePeerNode: ASDisplayNode { + private let avatarSelectionNode: ASImageNode + private let avatarNodeContainer: ASDisplayNode + private let avatarNode: AvatarNode + private var checkView: TGCheckButtonView? + private let textNode: ASTextNode + + var toggleSelection: (() -> Void)? + + private var currentSelected = false + + private var peer: Peer? + private var chatPeer: Peer? + + var textColor: UIColor = .black + var selectedColor: UIColor = UIColor(rgb: 0x007ee5) + + override init() { + self.avatarNodeContainer = ASDisplayNode() + + self.avatarSelectionNode = ASImageNode() + self.avatarSelectionNode.image = selectionBackgroundImage + self.avatarSelectionNode.isLayerBacked = true + self.avatarSelectionNode.displayWithoutProcessing = true + self.avatarSelectionNode.displaysAsynchronously = false + self.avatarSelectionNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) + self.avatarSelectionNode.alpha = 0.0 + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) + self.avatarNode.isLayerBacked = true + + self.textNode = ASTextNode() + self.textNode.isLayerBacked = true + self.textNode.displaysAsynchronously = true + + super.init() + + self.avatarNodeContainer.addSubnode(self.avatarSelectionNode) + self.avatarNodeContainer.addSubnode(self.avatarNode) + self.addSubnode(self.avatarNodeContainer) + self.addSubnode(self.textNode) + } + + func setup(account: Account, peer: Peer, chatPeer: Peer?, numberOfLines: Int = 2) { + self.peer = peer + self.chatPeer = chatPeer + + var defaultColor: UIColor = .black + if let chatPeer = chatPeer, chatPeer.id.namespace == Namespaces.Peer.SecretChat { + defaultColor = UIColor(rgb: 0x149a1f) + } + + let text = peer.displayTitle + self.textNode.maximumNumberOfLines = UInt(numberOfLines) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? UIColor(rgb: 0x007ee5) : defaultColor, paragraphAlignment: .center) + self.avatarNode.setPeer(account: account, peer: peer) + self.setNeedsLayout() + } + + func updateSelection(selected: Bool, animated: Bool) { + if selected != self.currentSelected { + self.currentSelected = selected + + if let peer = self.peer { + self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: selected ? self.selectedColor : textColor, paragraphAlignment: .center) + } + + if selected { + self.avatarNode.transform = CATransform3DMakeScale(0.866666, 0.866666, 1.0) + self.avatarSelectionNode.alpha = 1.0 + if animated { + //self.avatarNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.866666 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 10.0) + self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + self.avatarSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } else { + self.avatarNode.transform = CATransform3DIdentity + self.avatarSelectionNode.alpha = 0.0 + if animated { + //self.avatarNode.layer.animateSpring(from: 0.866666 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, initialVelocity: 10.0) + self.avatarNode.layer.animateScale(from: 0.866666, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.avatarSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28) + } + } + + self.checkView?.setSelected(selected, animated: animated) + } + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + let checkView = TGCheckButtonView(style: TGCheckButtonStyleShare)! + self.checkView = checkView + checkView.isUserInteractionEnabled = false + checkView.setSelected(self.currentSelected, animated: false) + self.view.addSubview(checkView) + + let avatarFrame = self.avatarNode.frame + let checkSize = checkView.bounds.size + checkView.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 22.0), size: checkSize) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.toggleSelection?() + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + self.avatarNodeContainer.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) + + self.textNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 4.0 + 60.0 + 4.0), size: CGSize(width: bounds.size.width - 4.0, height: 34.0)) + + let avatarFrame = self.avatarNode.frame + if let checkView = self.checkView { + let checkSize = checkView.bounds.size + checkView.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 22.0), size: checkSize) + } + } +} diff --git a/TelegramUI/ServiceSoundManager.swift b/TelegramUI/ServiceSoundManager.swift index ff55fafa9e..8f23deb348 100644 --- a/TelegramUI/ServiceSoundManager.swift +++ b/TelegramUI/ServiceSoundManager.swift @@ -39,6 +39,12 @@ public final class ServiceSoundManager { } } } + + public func playVibrationSound() { + self.queue.async { + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) + } + } } public let serviceSoundManager = ServiceSoundManager() diff --git a/TelegramUI/ShareActionButtonNode.swift b/TelegramUI/ShareActionButtonNode.swift index fe4eccca47..31c10e2f1f 100644 --- a/TelegramUI/ShareActionButtonNode.swift +++ b/TelegramUI/ShareActionButtonNode.swift @@ -4,7 +4,7 @@ import Display private let badgeBackgroundImage = generateStretchableFilledCircleImage(diameter: 22.0, color: UIColor(rgb: 0x007ee5)) -final class ShareActionButtonNode: ASButtonNode { +final class ShareActionButtonNode: HighlightTrackingButtonNode { private let badgeLabel: ASTextNode private let badgeBackground: ASImageNode @@ -44,6 +44,16 @@ final class ShareActionButtonNode: ASButtonNode { self.addSubnode(self.badgeBackground) self.addSubnode(self.badgeLabel) + + /*self.highligthedChanged = { [weak self] value in + if highlighted { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.highlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = ActionSheetItemNode.defaultBackgroundColor + }) + } + }*/ } override func layout() { @@ -53,7 +63,7 @@ final class ShareActionButtonNode: ASButtonNode { let badgeSize = self.badgeLabel.measure(CGSize(width: 100.0, height: 100.0)) let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 10.0 + 1.0), height: 22.0) - let backgroundFrame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 6.0, y: self.bounds.size.height - 39.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 6.0, y: self.bounds.size.height - 38.0), size: backgroundSize) self.badgeBackground.frame = backgroundFrame self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: backgroundFrame.minY + 2.0), size: badgeSize) diff --git a/TelegramUI/ShareContentContainerNode.swift b/TelegramUI/ShareContentContainerNode.swift new file mode 100644 index 0000000000..b556c2c222 --- /dev/null +++ b/TelegramUI/ShareContentContainerNode.swift @@ -0,0 +1,13 @@ +import Foundation +import UIKit +import Display +import Postbox + +protocol ShareContentContainerNode: class { + func activate() + func deactivate() + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) + func updateSelectedPeers() +} diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index d1a62ec73e..86c77dadd4 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -27,6 +27,11 @@ public struct ShareControllerAction { let action: () -> Void } +public enum ShareControllerSubject { + case url(String) + case message(Message) +} + public final class ShareController: ViewController { private var controllerNode: ShareControllerNode { return self.displayNode as! ShareControllerNode @@ -35,22 +40,48 @@ public final class ShareController: ViewController { private var animatedIn = false private let account: Account + private var presentationData: PresentationData + private let externalShare: Bool + private let subject: ShareControllerSubject + private let peers = Promise<[Peer]>() private let peersDisposable = MetaDisposable() - private let shareAction: ([PeerId]) -> Void - private let defaultAction: ShareControllerAction? + private var defaultAction: ShareControllerAction? public var dismissed: (() -> Void)? - public init(account: Account, shareAction: @escaping ([PeerId]) -> Void, defaultAction: ShareControllerAction?) { + public init(account: Account, subject: ShareControllerSubject, saveToCameraRoll: Bool = false ,externalShare: Bool = true) { self.account = account - self.shareAction = shareAction - self.defaultAction = defaultAction + self.externalShare = externalShare + self.subject = subject + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarTheme: nil) - self.peers.set(account.viewTracker.tailChatListView(count: 100) |> take(1) |> map { view -> [Peer] in + switch subject { + case let .url(text): + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Web_CopyLink, action: { [weak self] in + UIPasteboard.general.string = text + self?.controllerNode.cancel?() + }) + case let .message(message): + if saveToCameraRoll { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self?.saveToCameraRoll(message) + }) + } else if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { + if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Web_CopyLink, action: { [weak self] in + UIPasteboard.general.string = "https://t.me/\(addressName)/\(message.id.id)" + self?.controllerNode.cancel?() + }) + } + } + } + + self.peers.set(account.viewTracker.tailChatListView(count: 150) |> take(1) |> map { view -> [Peer] in var peers: [Peer] = [] for entry in view.0.entries.reversed() { switch entry { @@ -77,7 +108,9 @@ public final class ShareController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ShareControllerNode(account: self.account) + self.displayNode = ShareControllerNode(account: self.account, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in + self?.requestLayout(transition: transition) + }, externalShare: self.externalShare) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.dismissed?() @@ -85,8 +118,60 @@ public final class ShareController: ViewController { self.controllerNode.cancel = { [weak self] in self?.dismiss() } - self.controllerNode.share = { [weak self] peerIds in - self?.shareAction(peerIds) + self.controllerNode.share = { [weak self] text, peerIds in + if let strongSelf = self { + switch strongSelf.subject { + case let .url(url): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil)) + } + messages.append(.message(text: url, attributes: [], media: nil, replyToMessageId: nil)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + case let .message(message): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil)) + } + messages.append(.forward(source: message.id)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + } + } + return .complete() + } + self.controllerNode.shareExternal = { [weak self] in + if let strongSelf = self { + var activityItems: [Any] = [] + switch strongSelf.subject { + case let .url(text): + if let url = URL(string: text) { + activityItems.append(url) + } + case let .message(message): + if let chatPeer = message.peers[message.id.peerId] as? TelegramChannel { + if message.id.namespace == Namespaces.Message.Cloud, let addressName = chatPeer.addressName, !addressName.isEmpty { + if let url = URL(string: "https://t.me/\(addressName)/\(message.id.id)") { + activityItems.append("https://t.me/\(addressName)/\(message.id.id)" as NSString) + } + } + } + } + let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + if let window = strongSelf.view.window { + let legacyController = LegacyController(presentation: .modal(animateIn: false)) + let navigationController = UINavigationController() + legacyController.bind(controller: navigationController) + strongSelf.present(legacyController, in: .window(.root)) + navigationController.present(activityController, animated: true, completion: nil) + /*window.rootViewController?.present(activityController, animated: true, completion: { + + })*/ + } + } } self.displayNodeDidLoad() self.peersDisposable.set((self.peers.get() |> deliverOnMainQueue).start(next: { [weak self] next in @@ -121,4 +206,10 @@ public final class ShareController: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } + + private func saveToCameraRoll(_ message: Message) { + if let media = message.media.first { + self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(postbox: self.account.postbox, media: media)) + } + } } diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index 27bf5d16a7..f329e271e8 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -9,9 +9,6 @@ private let defaultBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 1.0) private let highlightedBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 1.0) private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) -private let subtitleFont = Font.regular(12.0) -private let subtitleColor = UIColor(rgb: 0x7b7b81) - private let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: .white) private let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: highlightedBackgroundColor) @@ -31,6 +28,11 @@ private let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account + private var presentationData: PresentationData + private let externalShare: Bool + + private let defaultAction: ShareControllerAction? + private let requestLayout: (ContainedViewLayoutTransition) -> Void private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -41,35 +43,40 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let contentContainerNode: ASDisplayNode private let contentBackgroundNode: ASImageNode - private let contentGridNode: GridNode - private let installActionButtonNode: ShareActionButtonNode - private let installActionSeparatorNode: ASDisplayNode - private let contentTitleNode: ASTextNode - private let contentSubtitleNode: ASTextNode - private let contentSeparatorNode: ASDisplayNode - private var activityIndicatorView: UIActivityIndicatorView? + private var contentNode: (ASDisplayNode & ShareContentContainerNode)? + private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? + private var animateContentNodeOffsetFromBackgroundOffset: CGFloat? + + private let actionsBackgroundNode: ASImageNode + private let actionButtonNode: ShareActionButtonNode + private let inputFieldNode: ShareInputFieldNode + private let actionSeparatorNode: ASDisplayNode var dismiss: (() -> Void)? var cancel: (() -> Void)? - var share: (([PeerId]) -> Void)? + var share: ((String, [PeerId]) -> Signal)? + var shareExternal: (() -> Void)? let ready = Promise() private var didSetReady = false - private var peers: [Peer]? - private var inProgress = false - private var peersUpdated = false - - private var didSetItems = false - - private var selectedPeers: [Peer] = [] private var controllerInteraction: ShareControllerInteraction? - private var defaultAction: ShareControllerAction? + private var peersContentNode: SharePeersContainerNode? - init(account: Account) { + private var scheduledLayoutTransitionRequestId: Int = 0 + private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + + private let shareDisposable = MetaDisposable() + + init(account: Account, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, externalShare: Bool) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.externalShare = externalShare + + self.defaultAction = defaultAction + self.requestLayout = requestLayout self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true @@ -83,47 +90,34 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.cancelButtonNode.displaysAsynchronously = false self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) - //self.cancelButtonNode.cornerRadius = 16.0 - //self.cancelButtonNode.clipsToBounds = true self.contentContainerNode = ASDisplayNode() - //self.contentContainerNode.cornerRadius = 16.0 - //self.contentContainerNode.clipsToBounds = true self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true self.contentBackgroundNode = ASImageNode() self.contentBackgroundNode.displaysAsynchronously = false self.contentBackgroundNode.displayWithoutProcessing = true self.contentBackgroundNode.image = roundedBackground - //self.contentBackgroundNode.cornerRadius = 16.0 - //self.contentBackgroundNode.clipsToBounds = true - self.contentGridNode = GridNode() + self.actionsBackgroundNode = ASImageNode() + self.actionsBackgroundNode.isLayerBacked = true + self.actionsBackgroundNode.displayWithoutProcessing = true + self.actionsBackgroundNode.displaysAsynchronously = false + self.actionsBackgroundNode.image = halfRoundedBackground - self.installActionButtonNode = ShareActionButtonNode() - self.installActionButtonNode.displaysAsynchronously = false - self.installActionButtonNode.titleNode.displaysAsynchronously = false - self.installActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal) - self.installActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + self.actionButtonNode = ShareActionButtonNode() + self.actionButtonNode.displaysAsynchronously = false + self.actionButtonNode.titleNode.displaysAsynchronously = false + self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) - self.contentTitleNode = ASTextNode() + self.inputFieldNode = ShareInputFieldNode(placeholder: self.presentationData.strings.ShareMenu_Comment) + self.inputFieldNode.alpha = 0.0 - self.contentSubtitleNode = ASTextNode() - self.contentSubtitleNode.maximumNumberOfLines = 1 - self.contentSubtitleNode.isLayerBacked = true - self.contentSubtitleNode.displaysAsynchronously = false - self.contentSubtitleNode.truncationMode = .byTruncatingTail - self.contentSubtitleNode.attributedText = NSAttributedString(string: "Select chats", font: subtitleFont, textColor: subtitleColor) - - self.contentSeparatorNode = ASDisplayNode() - self.contentSeparatorNode.isLayerBacked = true - self.contentSeparatorNode.displaysAsynchronously = false - self.contentSeparatorNode.backgroundColor = separatorColor - - self.installActionSeparatorNode = ASDisplayNode() - self.installActionSeparatorNode.isLayerBacked = true - self.installActionSeparatorNode.displaysAsynchronously = false - self.installActionSeparatorNode.backgroundColor = separatorColor + self.actionSeparatorNode = ASDisplayNode() + self.actionSeparatorNode.isLayerBacked = true + self.actionSeparatorNode.displaysAsynchronously = false + self.actionSeparatorNode.backgroundColor = separatorColor super.init() @@ -131,38 +125,30 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate if let strongSelf = self { if strongSelf.controllerInteraction!.selectedPeerIds.contains(peer.id) { strongSelf.controllerInteraction!.selectedPeerIds.remove(peer.id) - strongSelf.selectedPeers = strongSelf.selectedPeers.filter({ $0.id != peer.id }) + strongSelf.controllerInteraction!.selectedPeers = strongSelf.controllerInteraction!.selectedPeers.filter({ $0.id != peer.id }) } else { strongSelf.controllerInteraction!.selectedPeerIds.insert(peer.id) - strongSelf.selectedPeers.append(peer) + strongSelf.controllerInteraction!.selectedPeers.append(peer) + + strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.id) } - strongSelf.updateVisibleItemsSelection(animated: true) - if strongSelf.selectedPeers.isEmpty { - if let defaultAction = strongSelf.defaultAction { - strongSelf.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) - } else { - strongSelf.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: .gray, for: .normal) + let inputNodeAlpha: CGFloat = strongSelf.controllerInteraction!.selectedPeers.isEmpty ? 0.0 : 1.0 + if !strongSelf.inputFieldNode.alpha.isEqual(to: inputNodeAlpha) { + let previousAlpha = strongSelf.inputFieldNode.alpha + strongSelf.inputFieldNode.alpha = inputNodeAlpha + strongSelf.inputFieldNode.layer.animateAlpha(from: previousAlpha, to: inputNodeAlpha, duration: inputNodeAlpha.isZero ? 0.18 : 0.32) + + if inputNodeAlpha.isZero { + strongSelf.inputFieldNode.deactivateInput() } - strongSelf.installActionButtonNode.badge = nil - } else { - strongSelf.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) - strongSelf.installActionButtonNode.badge = "\(strongSelf.selectedPeers.count)" } - var subtitleText = "Select chats" - if !strongSelf.selectedPeers.isEmpty { - subtitleText = strongSelf.selectedPeers.reduce("", { string, peer in - if !string.isEmpty { - return string + ", " + peer.displayTitle - } else { - return string + peer.displayTitle - } - }) - } - strongSelf.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleColor) + strongSelf.updateButton() - if let (layout, navigationBarHeight) = strongSelf.containerLayout, let _ = strongSelf.peers { + strongSelf.contentNode?.updateSelectedPeers() + + if let (layout, navigationBarHeight) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } } @@ -177,50 +163,34 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) - self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) - /*self.cancelButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.cancelButtonNode.backgroundColor = highlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.cancelButtonNode.backgroundColor = defaultBackgroundColor - }) - } - } - }*/ - - /*self.installActionButtonNode.backgroundColor = defaultBackgroundColor - self.installActionButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.installActionButtonNode.backgroundColor = highlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.installActionButtonNode.backgroundColor = defaultBackgroundColor - }) - } - } - }*/ + self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) self.wrappingScrollNode.addSubnode(self.cancelButtonNode) self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.installActionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) + self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.contentContainerNode.addSubnode(self.contentGridNode) - self.contentContainerNode.addSubnode(self.installActionSeparatorNode) - self.contentContainerNode.addSubnode(self.installActionButtonNode) - self.wrappingScrollNode.addSubnode(self.contentTitleNode) - self.wrappingScrollNode.addSubnode(self.contentSubtitleNode) - self.wrappingScrollNode.addSubnode(self.contentSeparatorNode) + self.contentContainerNode.addSubnode(self.actionSeparatorNode) + self.contentContainerNode.addSubnode(self.actionsBackgroundNode) + self.contentContainerNode.addSubnode(self.actionButtonNode) + self.contentContainerNode.addSubnode(self.inputFieldNode) - self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in - self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let (layout, navigationBarHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.15, curve: .spring)) + } + } } + + self.updateButton() + } + + deinit { + self.shareDisposable.dispose() } override func didLoad() { @@ -231,12 +201,73 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + func transitionToContentNode(_ contentNode: (ASDisplayNode & ShareContentContainerNode)?, fastOut: Bool = false) { + if self.contentNode !== contentNode { + let transition: ContainedViewLayoutTransition + + let previous = self.contentNode + if let previous = previous { + previous.setContentOffsetUpdated(nil) + transition = .animated(duration: 0.4, curve: .spring) + + self.previousContentNode = previous + previous.alpha = 0.0 + previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: fastOut ? 0.1 : 0.2, removeOnCompletion: true, completion: { [weak self, weak previous] _ in + if let strongSelf = self, let previous = previous { + if strongSelf.previousContentNode === previous { + strongSelf.previousContentNode = nil + } + previous.removeFromSupernode() + } + }) + } else { + transition = .immediate + } + self.contentNode = contentNode + if let contentNode = contentNode { + contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + self.contentContainerNode.insertSubnode(contentNode, at: 0) + } + + if let (layout, navigationBarHeight) = self.containerLayout { + if let contentNode = contentNode, let previous = previous { + contentNode.alpha = 1.0 + let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.35) + animation.fillMode = kCAFillModeBoth + if !fastOut { + animation.beginTime = CACurrentMediaTime() + 0.1 + } + contentNode.layer.add(animation, forKey: "opacity") + + contentNode.frame = previous.frame + var bottomGridInset: CGFloat = 57.0 + + let inputHeight = self.inputFieldNode.bounds.size.height + + if !self.controllerInteraction!.selectedPeers.isEmpty { + bottomGridInset += inputHeight + } + self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY + self.scheduleInteractiveTransition(transition) + contentNode.activate() + previous.deactivate() + //self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } else { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) + self.scheduledLayoutTransitionRequest = nil transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - var insets = layout.insets(options: [.statusBar]) + var insets = layout.insets(options: [.statusBar, .input]) insets.top = max(10.0, insets.top) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -252,113 +283,54 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) - let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) - let contentFrame = contentContainerFrame.insetBy(dx: 12.0, dy: 0.0) + let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) - var insertItems: [GridNodeInsertItem] = [] + var bottomGridInset = buttonHeight - var itemCount = 0 - var animateIn = false + let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition) - if let peers = self.peers { - if let activityIndicatorView = self.activityIndicatorView { - activityIndicatorView.removeFromSuperview() - activityIndicatorView.stopAnimating() - } - itemCount = peers.count - if !self.didSetItems { - self.contentTitleNode.attributedText = NSAttributedString(string: "Share to", font: Font.medium(20.0), textColor: .black) - - self.didSetItems = true - animateIn = true - for i in 0 ..< peers.count { - insertItems.append(GridNodeInsertItem(index: i, item: ShareControllerPeerGridItem(account: self.account, peer: peers[i], controllerInteraction: self.controllerInteraction!), previousIndex: nil)) - } - } + if !self.controllerInteraction!.selectedPeers.isEmpty { + bottomGridInset += inputHeight } - 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) - self.contentTitleNode.frame = titleFrame - transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y)) - - let subtitleSize = self.contentSubtitleNode.measure(CGSize(width: contentContainerFrame.size.width - 44.0 * 2.0 - 4.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let subtitleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - subtitleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 40.0), size: subtitleSize) - let deltaSubtitlePosition = CGPoint(x: subtitleFrame.midX - self.contentSubtitleNode.frame.midX, y: subtitleFrame.midY - self.contentSubtitleNode.frame.midY) - self.contentSubtitleNode.frame = subtitleFrame - transition.animatePosition(node: self.contentSubtitleNode, from: CGPoint(x: subtitleFrame.midX, y: subtitleFrame.midY + deltaSubtitlePosition.y)) - - transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) - - let itemsPerRow = 4 - let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow)) - let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) - - let minimallyRevealedRowCount: CGFloat = 3.5 - let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) - - let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight) - let bottomGridInset = buttonHeight - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - if let activityIndicatorView = activityIndicatorView { - transition.updateFrame(layer: activityIndicatorView.layer, frame: CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - activityIndicatorView.bounds.size.width) / 2.0), y: contentFrame.maxY - activityIndicatorView.bounds.size.height - 34.0), size: activityIndicatorView.bounds.size)) - } + transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset))) - transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) - transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight))) + + transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) 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), 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 { - var durationOffset = 0.0 - self.contentGridNode.forEachRow { itemNodes in - for itemNode in itemNodes { - itemNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 4.0), to: CGPoint(), duration: 0.4 + durationOffset, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let itemNode = itemNode as? StickerPackPreviewGridItemNode { - itemNode.animateIn() - } - } - durationOffset += 0.04 - } - - self.contentGridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.installActionButtonNode.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.installActionSeparatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - - self.contentGridNode.layer.animateBoundsOriginYAdditive(from: -(topInset - buttonHeight), to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - } - - if let _ = self.peers, self.peersUpdated { - self.dequeueUpdatePeers() + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition) } } - private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { if let (layout, _) = self.containerLayout { - var insets = layout.insets(options: [.statusBar]) + var insets = layout.insets(options: [.statusBar, .input]) insets.top = max(10.0, insets.top) let bottomInset: CGFloat = 10.0 let buttonHeight: CGFloat = 57.0 let sectionSpacing: CGFloat = 8.0 - let titleAreaHeight: CGFloat = 64.0 let width = min(layout.size.width, layout.size.height) - 20.0 let sideInset = floor((layout.size.width - width) / 2.0) - let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - presentationLayout.contentOffset.y), size: contentFrame.size) + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size) if backgroundFrame.minY < contentFrame.minY { backgroundFrame.origin.y = contentFrame.minY } @@ -369,29 +341,17 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height backgroundFrame.size.height = buttonHeight + 32.0 } - var compactFrame = true - if let _ = self.peers, !inProgress { - compactFrame = false - } - if compactFrame { - backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.maxY - buttonHeight - 32.0), size: CGSize(width: contentFrame.size.width, height: buttonHeight + 32.0)) - } transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) - let titleSize = self.contentTitleNode.bounds.size - let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + 15.0), size: titleSize) - transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) - - let subtitleSize = self.contentSubtitleNode.bounds.size - let subtitleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - subtitleSize.width) / 2.0), y: backgroundFrame.minY + 40.0), size: subtitleSize) - transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) - - transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel))) - - if !compactFrame && CGFloat(0.0).isLessThanOrEqualTo(presentationLayout.contentOffset.y) { - self.contentSeparatorNode.alpha = 1.0 - } else { - self.contentSeparatorNode.alpha = 0.0 + if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { + self.animateContentNodeOffsetFromBackgroundOffset = nil + let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset + if let contentNode = self.contentNode { + transition.animatePositionAdditive(node: contentNode, offset: -offset) + } + if let previousContentNode = self.previousContentNode { + transition.updatePosition(node: previousContentNode, position: previousContentNode.position.offsetBy(dx: 0.0, dy: offset)) + } } } } @@ -407,16 +367,31 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } @objc func installActionButtonPressed() { - if self.selectedPeers.isEmpty { + if self.controllerInteraction!.selectedPeers.isEmpty { if let defaultAction = self.defaultAction { defaultAction.action() } } else { - self.share?(self.selectedPeers.map { $0.id }) - /*self.inProgress = true - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) - }*/ + self.inputFieldNode.deactivateInput() + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + + if let signal = self.share?(self.inputFieldNode.text, self.controllerInteraction!.selectedPeers.map { $0.id }) { + self.transitionToContentNode(ShareLoadingContainerNode(), fastOut: true) + let timestamp = CACurrentMediaTime() + self.shareDisposable.set(signal.start(completed: { [weak self] in + let minDelay = 1.2 + let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.cancel?() + } + }) + })) + } } } @@ -456,44 +431,51 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } func updatePeers(peers: [Peer], defaultAction: ShareControllerAction?) { - self.defaultAction = defaultAction + self.ready.set(.single(true)) - self.peers = peers + let peersContentNode = SharePeersContainerNode(account: self.account, strings: self.presentationData.strings, peers: peers, controllerInteraction: self.controllerInteraction!, externalShare: false && self.externalShare) + self.peersContentNode = peersContentNode + peersContentNode.openSearch = { [weak self] in + if let strongSelf = self { + let _ = (recentlySearchedPeers(postbox: strongSelf.account.postbox) + |> take(1) + |> deliverOnMainQueue).start(next: { peers in + if let strongSelf = self { + let searchContentNode = ShareSearchContainerNode(account: strongSelf.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction!, recentPeers: peers) + searchContentNode.cancel = { + if let strongSelf = self, let peersContentNode = strongSelf.peersContentNode { + strongSelf.transitionToContentNode(peersContentNode) + } + } + strongSelf.transitionToContentNode(searchContentNode) + } + }) + } + } + peersContentNode.openShare = { [weak self] in + self?.shareExternal?() + } + self.transitionToContentNode(peersContentNode) + + /*self.defaultAction = defaultAction + + self.peersContainerNode.peers = peers self.peersUpdated = true if let _ = self.containerLayout { self.dequeueUpdatePeers() } - self.installActionSeparatorNode.alpha = 1.0 + self.actionSeparatorNode.alpha = 1.0 if let defaultAction = defaultAction { - self.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) } else { - self.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: .gray, for: .normal) - } - } - - func dequeueUpdatePeers() { - if let (layout, navigationBarHeight) = self.containerLayout, let _ = peers, self.peersUpdated { - self.peersUpdated = false - - let transition: ContainedViewLayoutTransition - if self.didSetReady { - transition = .animated(duration: 0.4, curve: .spring) - } else { - transition = .immediate - } - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - if !self.didSetReady { - self.didSetReady = true - self.ready.set(.single(true)) - } - } + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: .gray, for: .normal) + }*/ } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) { + if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { return result } if self.bounds.contains(point) { @@ -513,11 +495,66 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } - private func updateVisibleItemsSelection(animated: Bool) { - self.contentGridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ShareControllerPeerGridItemNode { - itemNode.updateSelection(animated: animated) + private func scheduleInteractiveTransition(_ transition: ContainedViewLayoutTransition) { + if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { + switch scheduledLayoutTransitionRequest.1 { + case .immediate: + self.scheduleLayoutTransitionRequest(transition) + default: + break } + } else { + self.scheduleLayoutTransitionRequest(transition) } } + + private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { + let requestId = self.scheduledLayoutTransitionRequestId + self.scheduledLayoutTransitionRequestId += 1 + self.scheduledLayoutTransitionRequest = (requestId, transition) + (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in + if let strongSelf = self { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { + strongSelf.scheduledLayoutTransitionRequest = nil + strongSelf.requestLayout(currentRequestTransition) + } + } + }) + self.setNeedsLayout() + } + + private func updateButton() { + if self.controllerInteraction!.selectedPeers.isEmpty { + if let defaultAction = self.defaultAction { + self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + } else { + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: UIColor(rgb: 0x787878), for: .normal) + } + self.actionButtonNode.badge = nil + } else { + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + self.actionButtonNode.badge = "\(self.controllerInteraction!.selectedPeers.count)" + } + } + + func transitionToProgress(signal: Signal) { + self.inputFieldNode.deactivateInput() + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + + self.transitionToContentNode(ShareLoadingContainerNode(), fastOut: true) + let timestamp = CACurrentMediaTime() + self.shareDisposable.set(signal.start(completed: { [weak self] in + let minDelay = 1.2 + let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.cancel?() + } + }) + })) + } } diff --git a/TelegramUI/ShareControllerPeerGridItem.swift b/TelegramUI/ShareControllerPeerGridItem.swift index 9117dc10f9..d2b2c04071 100644 --- a/TelegramUI/ShareControllerPeerGridItem.swift +++ b/TelegramUI/ShareControllerPeerGridItem.swift @@ -7,6 +7,7 @@ import Postbox final class ShareControllerInteraction { var selectedPeerIds = Set() + var selectedPeers: [Peer] = [] let togglePeer: (Peer) -> Void init(togglePeer: @escaping (Peer) -> Void) { @@ -14,31 +15,92 @@ final class ShareControllerInteraction { } } -private let selectionBackgroundImage = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: 0x007ee5).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) -}) +final class ShareControllerGridSection: GridSection { + let height: CGFloat = 33.0 + + private let title: String + + var hashValue: Int { + return 1 + } + + init(title: String) { + self.title = title + } + + func isEqual(to: GridSection) -> Bool { + if let to = to as? ShareControllerGridSection { + return self.title == to.title + } else { + return false + } + } + + func node() -> ASDisplayNode { + return ShareControllerGridSectionNode(title: self.title) + } +} + +private let sectionTitleFont = Font.medium(12.0) + +final class ShareControllerGridSectionNode: ASDisplayNode { + let backgroundNode: ASDisplayNode + let titleNode: ASTextNode + + init(title: String) { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = UIColor(rgb: 0xf7f7f7) + + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.attributedText = NSAttributedString(string: title.uppercased(), font: sectionTitleFont, textColor: UIColor(rgb: 0x8e8e93)) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.titleNode) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: 27.0)) + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: titleSize) + } +} final class ShareControllerPeerGridItem: GridItem { let account: Account let peer: Peer + let chatPeer: Peer? let controllerInteraction: ShareControllerInteraction - let section: GridSection? = nil + let section: GridSection? - init(account: Account, peer: Peer, controllerInteraction: ShareControllerInteraction) { + init(account: Account, peer: Peer, chatPeer: Peer?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil) { self.account = account self.peer = peer + self.chatPeer = chatPeer self.controllerInteraction = controllerInteraction + + if let sectionTitle = sectionTitle { + self.section = ShareControllerGridSection(title: sectionTitle) + } else { + self.section = nil + } } func node(layout: GridNodeLayout) -> GridItemNode { let node = ShareControllerPeerGridItemNode() node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, peer: self.peer) + node.setup(account: self.account, peer: self.peer, chatPeer: self.chatPeer) return node } @@ -48,121 +110,59 @@ final class ShareControllerPeerGridItem: GridItem { return } node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, peer: self.peer) + node.setup(account: self.account, peer: self.peer, chatPeer: self.chatPeer) } } -private let avatarFont = Font.medium(18.0) -private let textFont = Font.regular(11.0) - final class ShareControllerPeerGridItemNode: GridItemNode { - private var currentState: (Account, Peer)? - private let avatarSelectionNode: ASImageNode - private let avatarNodeContainer: ASDisplayNode - private let avatarNode: AvatarNode - private let textNode: ASTextNode + private var currentState: (Account, Peer, Peer?)? + private let peerNode: SelectablePeerNode var controllerInteraction: ShareControllerInteraction? - var currentSelected = false - override init() { - self.avatarNodeContainer = ASDisplayNode() - - self.avatarSelectionNode = ASImageNode() - self.avatarSelectionNode.image = selectionBackgroundImage - self.avatarSelectionNode.isLayerBacked = true - self.avatarSelectionNode.displayWithoutProcessing = true - self.avatarSelectionNode.displaysAsynchronously = false - self.avatarSelectionNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) - self.avatarSelectionNode.alpha = 0.0 - - self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) - self.avatarNode.isLayerBacked = true - - self.textNode = ASTextNode() - self.textNode.isLayerBacked = true - self.textNode.displaysAsynchronously = true - self.textNode.maximumNumberOfLines = 2 + self.peerNode = SelectablePeerNode() super.init() - self.avatarNodeContainer.addSubnode(self.avatarSelectionNode) - self.avatarNodeContainer.addSubnode(self.avatarNode) - self.addSubnode(self.avatarNodeContainer) - self.addSubnode(self.textNode) + self.peerNode.toggleSelection = { [weak self] in + if let strongSelf = self { + if let (_, peer, chatPeer) = strongSelf.currentState { + let mainPeer = chatPeer ?? peer + strongSelf.controllerInteraction?.togglePeer(mainPeer) + } + } + } + self.addSubnode(self.peerNode) } - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - func setup(account: Account, peer: Peer) { + func setup(account: Account, peer: Peer, chatPeer: Peer?) { if self.currentState == nil || self.currentState!.0 !== account || !arePeersEqual(self.currentState!.1, peer) { - let text = peer.displayTitle - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? UIColor(rgb: 0x007ee5) : UIColor.black, paragraphAlignment: .center) - self.avatarNode.setPeer(account: account, peer: peer) - self.currentState = (account, peer) + self.peerNode.setup(account: account, peer: peer, chatPeer: chatPeer) + self.currentState = (account, peer, chatPeer) self.setNeedsLayout() } self.updateSelection(animated: false) } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let (_, peer) = self.currentState { - self.controllerInteraction?.togglePeer(peer) - } - } - } - func updateSelection(animated: Bool) { var selected = false - if let controllerInteraction = self.controllerInteraction, let (_, peer) = self.currentState { - selected = controllerInteraction.selectedPeerIds.contains(peer.id) + if let controllerInteraction = self.controllerInteraction, let (_, peer, chatPeer) = self.currentState { + let mainPeer = chatPeer ?? peer + selected = controllerInteraction.selectedPeerIds.contains(mainPeer.id) } - if selected != self.currentSelected { - self.currentSelected = selected - - if let (_, peer) = self.currentState { - self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: selected ? UIColor(rgb: 0x007ee5) : UIColor.black, paragraphAlignment: .center) - } - - if selected { - self.avatarNode.transform = CATransform3DMakeScale(0.866666, 0.866666, 1.0) - self.avatarSelectionNode.alpha = 1.0 - if animated { - //self.avatarNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.866666 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 10.0) - self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - self.avatarSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } else { - self.avatarNode.transform = CATransform3DIdentity - self.avatarSelectionNode.alpha = 0.0 - if animated { - //self.avatarNode.layer.animateSpring(from: 0.866666 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, initialVelocity: 10.0) - self.avatarNode.layer.animateScale(from: 0.866666, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - self.avatarSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28) - } - } - } + self.peerNode.updateSelection(selected: selected, animated: animated) } override func layout() { super.layout() let bounds = self.bounds - - self.avatarNodeContainer.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) - - self.textNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 4.0 + 60.0 + 4.0), size: CGSize(width: bounds.size.width - 4.0, height: 34.0)) + self.peerNode.frame = bounds } func animateIn() { - self.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 60.0), to: CGPoint(), duration: 0.42, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.peerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 60.0), to: CGPoint(), duration: 0.42, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } diff --git a/TelegramUI/ShareControllerRecentPeersGridItem.swift b/TelegramUI/ShareControllerRecentPeersGridItem.swift new file mode 100644 index 0000000000..2577f02aaa --- /dev/null +++ b/TelegramUI/ShareControllerRecentPeersGridItem.swift @@ -0,0 +1,84 @@ +import Foundation +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox + +final class ShareControllerRecentPeersGridItem: GridItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let controllerInteraction: ShareControllerInteraction + + let section: GridSection? = nil + let fillsRowWithHeight: CGFloat? = 130.0 + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction) { + self.account = account + self.theme = theme + self.strings = strings + self.controllerInteraction = controllerInteraction + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = ShareControllerRecentPeersGridItemNode() + node.controllerInteraction = self.controllerInteraction + node.setup(account: self.account, theme: self.theme, strings: self.strings) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? ShareControllerRecentPeersGridItemNode else { + assertionFailure() + return + } + node.controllerInteraction = self.controllerInteraction + node.setup(account: self.account, theme: self.theme, strings: self.strings) + } +} + +final class ShareControllerRecentPeersGridItemNode: GridItemNode { + private var currentState: (Account, PresentationTheme, PresentationStrings)? + + var controllerInteraction: ShareControllerInteraction? + + private var peersNode: ChatListSearchRecentPeersNode? + + override init() { + super.init() + } + + func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + if self.currentState == nil || self.currentState!.0 !== account { + let peersNode: ChatListSearchRecentPeersNode + if let currentPeersNode = self.peersNode { + peersNode = currentPeersNode + peersNode.updateThemeAndStrings(theme: theme, strings: strings) + } else { + peersNode = ChatListSearchRecentPeersNode(account: account, theme: theme, strings: strings, peerSelected: { [weak self] peer in + self?.controllerInteraction?.togglePeer(peer) + }, isPeerSelected: { [weak self] peerId in + return self?.controllerInteraction?.selectedPeerIds.contains(peerId) ?? false + }, share: true) + self.peersNode = peersNode + self.addSubnode(peersNode) + } + + self.currentState = (account, theme, strings) + } + self.updateSelection(animated: false) + } + + func updateSelection(animated: Bool) { + self.peersNode?.updateSelectedPeers(animated: animated) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + self.peersNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) + } +} diff --git a/TelegramUI/ShareInputFieldNode.swift b/TelegramUI/ShareInputFieldNode.swift new file mode 100644 index 0000000000..cac6a4ff4a --- /dev/null +++ b/TelegramUI/ShareInputFieldNode.swift @@ -0,0 +1,133 @@ +import Foundation +import AsyncDisplayKit +import Display + +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + +private let backgroundImage = generateStretchableFilledCircleImage(diameter: 6.0, color: UIColor(rgb: 0xe9e9e9)) + +final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private let backgroundNode: ASImageNode + private let textInputNode: ASEditableTextNode + private let placeholderNode: ASTextNode + private let clearButton: HighlightableButtonNode + + var updateHeight: (() -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 1.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 8.0) + private let accessoryButtonsWidth: CGFloat = 10.0 + + var text: String { + return self.textInputNode.attributedText?.string ?? "" + } + + init(placeholder: String) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = backgroundImage + + self.textInputNode = ASEditableTextNode() + let textColor: UIColor = .black + let keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default + textInputNode.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(17.0), NSAttributedStringKey.foregroundColor.rawValue: textColor] + textInputNode.clipsToBounds = true + textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + textInputNode.keyboardAppearance = keyboardAppearance + textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + + self.placeholderNode = ASTextNode() + self.placeholderNode.isLayerBacked = true + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(rgb: 0x818086)) + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateClearIcon(color: UIColor(rgb: 0x7b7b81)), for: []) + self.clearButton.isHidden = true + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + self.addSubnode(self.clearButton) + + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + let accessoryButtonsWidth = self.accessoryButtonsWidth + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.size.width - placeholderSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + if let image = self.clearButton.image(for: []) { + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) + } + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: backgroundFrame.size.height))) + + return panelHeight + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + } + + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.placeholderNode.isHidden = true + self.clearButton.isHidden = false + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.placeholderNode.isHidden = false + self.clearButton.isHidden = true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + let accessoryButtonsWidth = self.accessoryButtonsWidth + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(41.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.attributedText = nil + self.deactivateInput() + } +} diff --git a/TelegramUI/ShareLoadingContainerNode.swift b/TelegramUI/ShareLoadingContainerNode.swift new file mode 100644 index 0000000000..68b6a134f6 --- /dev/null +++ b/TelegramUI/ShareLoadingContainerNode.swift @@ -0,0 +1,43 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox + +final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContainerNode { + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + private let activityIndicator: ActivityIndicator + + override init() { + self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(UIColor(rgb: 0x007ee5))) + + super.init() + + self.addSubnode(self.activityIndicator) + } + + func activate() { + } + + func deactivate() { + } + + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let nodeHeight: CGFloat = 125.0 + + let indicatorSize = self.activityIndicator.calculateSizeThatFits(size) + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: size.height - nodeHeight + floor((nodeHeight - indicatorSize.height) / 2.0)), size: indicatorSize)) + + self.contentOffsetUpdated?(-size.height + 64.0, transition) + } + + func updateSelectedPeers() { + } +} diff --git a/TelegramUI/SharePeersContainerNode.swift b/TelegramUI/SharePeersContainerNode.swift new file mode 100644 index 0000000000..f2c69bff01 --- /dev/null +++ b/TelegramUI/SharePeersContainerNode.swift @@ -0,0 +1,265 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) + +private let subtitleFont = Font.regular(12.0) +private let subtitleColor = UIColor(rgb: 0x7b7b81) + +final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { + private let account: Account + private let strings: PresentationStrings + private let controllerInteraction: ShareControllerInteraction + var peers: [Peer]? + + private let contentGridNode: GridNode + private let contentTitleNode: ASTextNode + private let contentSubtitleNode: ASTextNode + private let contentSeparatorNode: ASDisplayNode + private let searchButtonNode: HighlightableButtonNode + private let shareButtonNode: HighlightableButtonNode + + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + var openSearch: (() -> Void)? + var openShare: (() -> Void)? + + private var ensurePeerVisibleOnLayout: PeerId? + private var validLayout: (CGSize, CGFloat)? + private var overrideGridOffsetTransition: ContainedViewLayoutTransition? + + init(account: Account, strings: PresentationStrings, peers: [Peer], controllerInteraction: ShareControllerInteraction, externalShare: Bool) { + self.account = account + self.strings = strings + self.controllerInteraction = controllerInteraction + self.peers = peers + + self.contentGridNode = GridNode() + + self.contentTitleNode = ASTextNode() + self.contentTitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: .black) + + self.contentSubtitleNode = ASTextNode() + self.contentSubtitleNode.maximumNumberOfLines = 1 + self.contentSubtitleNode.isLayerBacked = true + self.contentSubtitleNode.displaysAsynchronously = false + self.contentSubtitleNode.truncationMode = .byTruncatingTail + self.contentSubtitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_SelectChats, font: subtitleFont, textColor: subtitleColor) + + self.searchButtonNode = HighlightableButtonNode() + self.searchButtonNode.setImage(UIImage(bundleImageName: "Share/SearchIcon")?.precomposed(), for: []) + + self.shareButtonNode = HighlightableButtonNode() + self.shareButtonNode.setImage(UIImage(bundleImageName: "Share/ShareIcon")?.precomposed(), for: []) + self.shareButtonNode.isHidden = !externalShare + + self.contentSeparatorNode = ASDisplayNode() + self.contentSeparatorNode.isLayerBacked = true + self.contentSeparatorNode.displaysAsynchronously = false + self.contentSeparatorNode.backgroundColor = separatorColor + + super.init() + + self.addSubnode(self.contentGridNode) + + self.addSubnode(self.contentTitleNode) + self.addSubnode(self.contentSubtitleNode) + self.addSubnode(self.searchButtonNode) + self.addSubnode(self.shareButtonNode) + self.addSubnode(self.contentSeparatorNode) + + var insertItems: [GridNodeInsertItem] = [] + for i in 0 ..< peers.count { + insertItems.append(GridNodeInsertItem(index: i, item: ShareControllerPeerGridItem(account: self.account, peer: peers[i], chatPeer: nil, controllerInteraction: self.controllerInteraction), previousIndex: nil)) + } + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + + self.searchButtonNode.addTarget(self, action: #selector(self.searchPressed), forControlEvents: .touchUpInside) + self.shareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) + } + + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { + self.ensurePeerVisibleOnLayout = peerId + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) { + let itemCount = self.peers?.count ?? 1 + + let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) + let minimalItemWidth: CGFloat = size.width > 301.0 ? 70.0 : 60.0 + let effectiveWidth = size.width - itemInsets.left - itemInsets.right + + let itemsPerRow = Int(effectiveWidth / minimalItemWidth) + + let itemWidth = floor(effectiveWidth / CGFloat(itemsPerRow)) + var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) + rowCount = max(rowCount, 4) + + let minimallyRevealedRowCount: CGFloat = 3.7 + let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) + + let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) + return (gridTopInset, itemWidth) + } + + func activate() { + } + + func deactivate() { + } + + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let firstLayout = self.validLayout == nil + self.validLayout = (size, bottomInset) + + let gridLayoutTransition: ContainedViewLayoutTransition + if firstLayout { + gridLayoutTransition = .immediate + self.overrideGridOffsetTransition = transition + } else { + gridLayoutTransition = transition + self.overrideGridOffsetTransition = nil + } + + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + + var scrollToItem: GridNodeScrollToItem? + if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { + self.ensurePeerVisibleOnLayout = nil + if let index = self.peers?.index(where: { $0.id == ensurePeerVisibleOnLayout }) { + scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false) + } + } + + let gridSize = CGSize(width: size.width - 12.0, height: size.height) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + gridLayoutTransition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 0.0), size: gridSize)) + + if firstLayout { + self.animateIn() + } + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + guard let (size, _) = self.validLayout else { + return + } + + let actualTransition = self.overrideGridOffsetTransition ?? transition + self.overrideGridOffsetTransition = nil + + let titleAreaHeight: CGFloat = 64.0 + + let rawTitleOffset = -titleAreaHeight - presentationLayout.contentOffset.y + let titleOffset = max(-titleAreaHeight, rawTitleOffset) + + let titleSize = self.contentTitleNode.measure(size) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: titleOffset + 15.0), size: titleSize) + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + + let subtitleSize = self.contentSubtitleNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: titleAreaHeight)) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleOffset + 40.0), size: subtitleSize) + var originalSubtitleFrame = self.contentSubtitleNode.frame + originalSubtitleFrame.origin.x = subtitleFrame.origin.x + originalSubtitleFrame.size = subtitleFrame.size + self.contentSubtitleNode.frame = originalSubtitleFrame + transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) + + let titleButtonSize = CGSize(width: 44.0, height: 44.0) + let searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: titleOffset + 12.0), size: titleButtonSize) + transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame) + + let shareButtonFrame = CGRect(origin: CGPoint(x: size.width - titleButtonSize.width - 12.0, y: titleOffset + 12.0), size: titleButtonSize) + transition.updateFrame(node: self.shareButtonNode, frame: shareButtonFrame) + + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleOffset + titleAreaHeight), size: CGSize(width: size.width, height: UIScreenPixel))) + + if rawTitleOffset.isLess(than: -titleAreaHeight) { + self.contentSeparatorNode.alpha = 1.0 + } else { + self.contentSeparatorNode.alpha = 0.0 + } + + self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) + } + + func updateVisibleItemsSelection(animated: Bool) { + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ShareControllerPeerGridItemNode { + itemNode.updateSelection(animated: animated) + } + } + } + + func animateIn() { + var durationOffset = 0.0 + self.contentGridNode.forEachRow { itemNodes in + for itemNode in itemNodes { + itemNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 4.0), to: CGPoint(), duration: 0.4 + durationOffset, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let itemNode = itemNode as? StickerPackPreviewGridItemNode { + itemNode.animateIn() + } + } + durationOffset += 0.04 + } + + if let (size, _) = self.validLayout { + let (topInset, _) = self.calculateMetrics(size: size) + self.contentGridNode.layer.animateBoundsOriginYAdditive(from: -(topInset - 64.0), to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func updateSelectedPeers() { + var subtitleText = self.strings.ShareMenu_SelectChats + if !self.controllerInteraction.selectedPeers.isEmpty { + subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in + if !string.isEmpty { + return string + ", " + peer.displayTitle + } else { + return string + peer.displayTitle + } + }) + } + self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleColor) + + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ShareControllerPeerGridItemNode { + itemNode.updateSelection(animated: true) + } + } + } + + @objc func searchPressed() { + self.openSearch?() + } + + @objc func sharePressed() { + self.openShare?() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode] + for node in nodes { + let nodeFrame = node.frame + if let result = node.hitTest(point.offsetBy(dx: -nodeFrame.minX, dy: -nodeFrame.minY), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } +} diff --git a/TelegramUI/ShareSearchBarNode.swift b/TelegramUI/ShareSearchBarNode.swift new file mode 100644 index 0000000000..c156b50754 --- /dev/null +++ b/TelegramUI/ShareSearchBarNode.swift @@ -0,0 +1,102 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let searchIconImage = UIImage(bundleImageName: "Share/SearchBarSearchIcon")?.precomposed() +private let backgroundImage = generateStretchableFilledCircleImage(diameter: 6.0, color: UIColor(rgb: 0xe2e2e2)) +private let placeholderColor = UIColor(rgb: 0x7b7b81) + +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + +final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate { + private let backgroundNode: ASImageNode + private let searchIconNode: ASImageNode + private let textInputNode: TextFieldNode + private let clearButton: HighlightableButtonNode + + private let inputInsets = UIEdgeInsets(top: 10.0, left: 26.0, bottom: 10.0, right: 10.0 + 16.0) + + var textUpdated: ((String) -> Void)? + + init(placeholder: String) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = backgroundImage + + self.searchIconNode = ASImageNode() + self.searchIconNode.isLayerBacked = true + self.searchIconNode.displaysAsynchronously = false + self.searchIconNode.displayWithoutProcessing = true + self.searchIconNode.image = searchIconImage + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateClearIcon(color: UIColor(rgb: 0x7b7b81)), for: []) + self.clearButton.isHidden = true + + self.textInputNode = TextFieldNode() + self.textInputNode.fixOffset = false + let textColor: UIColor = .black + let keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default + textInputNode.textField.font = Font.regular(16.0) + textInputNode.textField.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(16.0), NSAttributedStringKey.foregroundColor.rawValue: textColor] + textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + textInputNode.textField.keyboardAppearance = keyboardAppearance + textInputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: placeholderColor) + + super.init() + + self.textInputNode.textField.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.searchIconNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.clearButton) + + self.textInputNode.textField.addTarget(self, action: #selector(self.textFieldDidChangeText), for: [.editingChanged]) + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) { + let inputInsets = self.inputInsets + + let textFieldHeight: CGFloat = 40.0 + + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: textFieldHeight)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + if let image = self.searchIconNode.image { + transition.updateFrame(node: self.searchIconNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + 8.0, y: backgroundFrame.minY + 13.0), size: image.size)) + } + + if let image = self.clearButton.image(for: []) { + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) + } + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + UIScreenPixel), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + } + + func activateInput() { + self.textInputNode.textField.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.textField.resignFirstResponder() + } + + @objc func textFieldDidChangeText() { + self.clearButton.isHidden = self.textInputNode.textField.text?.isEmpty ?? true + self.textUpdated?(self.textInputNode.textField.text ?? "") + } + + @objc func clearPressed() { + self.textInputNode.textField.text = "" + self.textFieldDidChangeText() + } +} diff --git a/TelegramUI/ShareSearchContainerNode.swift b/TelegramUI/ShareSearchContainerNode.swift new file mode 100644 index 0000000000..d04fae2027 --- /dev/null +++ b/TelegramUI/ShareSearchContainerNode.swift @@ -0,0 +1,570 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) + +private let cancelFont = Font.regular(17.0) +private let cancelColor = UIColor(rgb: 0x007ee5) +private let subtitleFont = Font.regular(12.0) +private let subtitleColor = UIColor(rgb: 0x7b7b81) + +private enum ShareSearchRecentEntryStableId: Hashable { + case topPeers + case peerId(PeerId) + + static func ==(lhs: ShareSearchRecentEntryStableId, rhs: ShareSearchRecentEntryStableId) -> Bool { + switch lhs { + case .topPeers: + if case .topPeers = rhs { + return true + } else { + return false + } + case let .peerId(peerId): + if case .peerId(peerId) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case .topPeers: + return 0 + case let .peerId(peerId): + return peerId.hashValue + } + } +} + +private enum ShareSearchRecentEntry: Comparable, Identifiable { + case topPeers(PresentationTheme, PresentationStrings) + case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationStrings) + + var stableId: ShareSearchRecentEntryStableId { + switch self { + case .topPeers: + return .topPeers + case let .peer(_, peer, _, _): + return .peerId(peer.id) + } + } + + static func ==(lhs: ShareSearchRecentEntry, rhs: ShareSearchRecentEntry) -> Bool { + switch lhs { + case let .topPeers(lhsTheme, lhsStrings): + if case let .topPeers(rhsTheme, rhsStrings) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + } else { + return false + } + case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsStrings): + if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsStrings === rhsStrings { + return true + } else { + return false + } + } + } + + static func <(lhs: ShareSearchRecentEntry, rhs: ShareSearchRecentEntry) -> Bool { + switch lhs { + case .topPeers: + return true + case let .peer(lhsIndex, _, _, _): + switch rhs { + case .topPeers: + return false + case let .peer(rhsIndex, _, _, _): + return lhsIndex <= rhsIndex + } + } + } + + func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { + switch self { + case let .topPeers(theme, strings): + return ShareControllerRecentPeersGridItem(account: account, theme: theme, strings: strings, controllerInteraction: interfaceInteraction) + case let .peer(_, peer, associatedPeer, strings): + let primaryPeer: Peer + var chatPeer: Peer? + if let associatedPeer = associatedPeer { + primaryPeer = associatedPeer + chatPeer = peer + } else { + primaryPeer = peer + chatPeer = associatedPeer + } + return ShareControllerPeerGridItem(account: account, peer: primaryPeer, chatPeer: chatPeer, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent) + } + } +} + +private struct ShareSearchPeerEntry: Comparable, Identifiable { + let index: Int32 + let peer: Peer + + var stableId: Int64 { + return self.peer.id.toInt64() + } + + static func ==(lhs: ShareSearchPeerEntry, rhs: ShareSearchPeerEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if !arePeersEqual(lhs.peer, rhs.peer) { + return false + } + return true + } + + static func <(lhs: ShareSearchPeerEntry, rhs: ShareSearchPeerEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { + return ShareControllerPeerGridItem(account: account, peer: self.peer, chatPeer: nil, controllerInteraction: interfaceInteraction) + } +} + +private struct ShareSearchGridTransaction { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let animated: Bool +} + +private func preparedGridEntryTransition(account: Account, from fromEntries: [ShareSearchPeerEntry], to toEntries: [ShareSearchPeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + 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, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + + return ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) +} + +private func preparedRecentEntryTransition(account: Account, from fromEntries: [ShareSearchRecentEntry], to toEntries: [ShareSearchRecentEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + 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, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + + return ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) +} + +final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { + private let account: Account + private let strings: PresentationStrings + private let controllerInteraction: ShareControllerInteraction + + private var entries: [ShareSearchPeerEntry] = [] + private var recentEntries: [ShareSearchRecentEntry] = [] + + private var enqueuedTransitions: [(ShareSearchGridTransaction, Bool)] = [] + private var enqueuedRecentTransitions: [(ShareSearchGridTransaction, Bool)] = [] + + private let contentGridNode: GridNode + private let recentGridNode: GridNode + + private let contentSeparatorNode: ASDisplayNode + private let searchNode: ShareSearchBarNode + private let cancelButtonNode: HighlightableButtonNode + + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + var cancel: (() -> Void)? + + private var ensurePeerVisibleOnLayout: PeerId? + private var validLayout: (CGSize, CGFloat)? + private var overrideGridOffsetTransition: ContainedViewLayoutTransition? + + private let recentDisposable = MetaDisposable() + + private let searchQuery = ValuePromise("", ignoreRepeated: true) + private let searchDisposable = MetaDisposable() + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction, recentPeers: [RenderedPeer]) { + self.account = account + self.strings = strings + self.controllerInteraction = controllerInteraction + + self.recentGridNode = GridNode() + self.contentGridNode = GridNode() + self.contentGridNode.isHidden = true + + self.searchNode = ShareSearchBarNode(placeholder: strings.Common_Search) + + self.cancelButtonNode = HighlightableButtonNode() + self.cancelButtonNode.setTitle(strings.Common_Cancel, with: cancelFont, with: cancelColor, for: []) + self.cancelButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + + self.contentSeparatorNode = ASDisplayNode() + self.contentSeparatorNode.isLayerBacked = true + self.contentSeparatorNode.displaysAsynchronously = false + self.contentSeparatorNode.backgroundColor = separatorColor + + super.init() + + self.addSubnode(self.recentGridNode) + self.addSubnode(self.contentGridNode) + + self.addSubnode(self.searchNode) + self.addSubnode(self.cancelButtonNode) + self.addSubnode(self.contentSeparatorNode) + + self.recentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + if let strongSelf = self, !strongSelf.recentGridNode.isHidden { + strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + } + + self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + if let strongSelf = self, !strongSelf.contentGridNode.isHidden { + strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + } + + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + + let foundItems = searchQuery.get() + |> mapToSignal { query -> Signal<[ShareSearchPeerEntry]?, NoError> in + if !query.isEmpty { + let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased()) + let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + + return combineLatest(foundLocalPeers, foundRemotePeers) + |> map { foundLocalPeers, foundRemotePeers -> [ShareSearchPeerEntry]? in + var entries: [ShareSearchPeerEntry] = [] + var index: Int32 = 0 + for renderedPeer in foundLocalPeers { + if let peer = renderedPeer.peers[renderedPeer.peerId] { + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + //entries.append(.localPeer(peer, associatedPeer, index, themeAndStrings.0, themeAndStrings.1)) + entries.append(ShareSearchPeerEntry(index: index, peer: peer)) + index += 1 + } + } + + for peer in foundRemotePeers { + entries.append(ShareSearchPeerEntry(index: index, peer: peer)) + //entries.append(.globalPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) + index += 1 + } + + return entries + } + } else { + return .single(nil) + } + } + + let previousSearchItems = Atomic<[ShareSearchPeerEntry]?>(value: nil) + self.searchDisposable.set((foundItems + |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + let previousEntries = previousSearchItems.swap(entries) + strongSelf.entries = entries ?? [] + + let firstTime = previousEntries == nil + let transition = preparedGridEntryTransition(account: account, from: previousEntries ?? [], to: entries ?? [], interfaceInteraction: controllerInteraction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + + if (previousEntries == nil) != (entries == nil) { + if previousEntries == nil { + strongSelf.recentGridNode.isHidden = true + strongSelf.contentGridNode.isHidden = false + strongSelf.transitionToContentGridLayout() + } else { + strongSelf.recentGridNode.isHidden = false + strongSelf.contentGridNode.isHidden = true + strongSelf.transitionToRecentGridLayout() + } + } + } + })) + + self.searchNode.textUpdated = { [weak self] text in + self?.searchQuery.set(text) + } + + var recentItemList: [ShareSearchRecentEntry] = [] + recentItemList.append(.topPeers(theme, strings)) + var index = 0 + for peer in recentPeers { + if let mainPeer = peer.peers[peer.peerId] { + recentItemList.append(.peer(index: index, peer: mainPeer, associatedPeer: mainPeer.associatedPeerId.flatMap { peer.peers[$0] }, strings)) + index += 1 + } + } + + let recentItems: Signal<[ShareSearchRecentEntry], NoError> = .single(recentItemList) + let previousRecentItems = Atomic<[ShareSearchRecentEntry]?>(value: nil) + self.recentDisposable.set((recentItems + |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + let previousEntries = previousRecentItems.swap(entries) + strongSelf.recentEntries = entries + + let firstTime = previousEntries == nil + let transition = preparedRecentEntryTransition(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) + strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) + } + })) + } + + deinit { + self.searchDisposable.dispose() + self.recentDisposable.dispose() + } + + func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { + self.ensurePeerVisibleOnLayout = peerId + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + func activate() { + self.searchNode.activateInput() + } + + func deactivate() { + self.searchNode.deactivateInput() + } + + private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) { + let itemCount: Int + if self.contentGridNode.isHidden { + itemCount = self.recentEntries.count + } else { + itemCount = self.entries.count + } + + let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) + let minimalItemWidth: CGFloat = 70.0 + let effectiveWidth = size.width - itemInsets.left - itemInsets.right + + let itemsPerRow = Int(effectiveWidth / minimalItemWidth) + + let itemWidth = floor(effectiveWidth / CGFloat(itemsPerRow)) + var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) + rowCount = max(rowCount, 4) + + let minimallyRevealedRowCount: CGFloat = 3.7 + let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) + + let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) + return (gridTopInset, itemWidth) + } + + func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let firstLayout = self.validLayout == nil + self.validLayout = (size, bottomInset) + + let gridLayoutTransition: ContainedViewLayoutTransition + if firstLayout { + gridLayoutTransition = .immediate + self.overrideGridOffsetTransition = transition + } else { + gridLayoutTransition = transition + self.overrideGridOffsetTransition = nil + } + + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + + var scrollToItem: GridNodeScrollToItem? + if !self.contentGridNode.isHidden, let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { + self.ensurePeerVisibleOnLayout = nil + if let index = self.entries.index(where: { $0.peer.id == ensurePeerVisibleOnLayout }) { + scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false) + } + } + + var scrollToRecentItem: GridNodeScrollToItem? + if !self.recentGridNode.isHidden, let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { + self.ensurePeerVisibleOnLayout = nil + if let index = self.recentEntries.index(where: { + switch $0 { + case .topPeers: + return false + case let .peer(_, peer, _, _): + return peer.id == ensurePeerVisibleOnLayout + } + }) { + scrollToRecentItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false) + } + } + + let gridSize = CGSize(width: size.width, height: size.height - 5.0) + + self.recentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToRecentItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + gridLayoutTransition.updateFrame(node: self.recentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 5.0), size: gridSize)) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + gridLayoutTransition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 5.0), size: gridSize)) + + if firstLayout { + self.animateIn() + + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + + while !self.enqueuedRecentTransitions.isEmpty { + self.dequeueRecentTransition() + } + } + } + + private func transitionToRecentGridLayout(_ transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)) { + if let (size, bottomInset) = self.validLayout { + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + + let offset = self.recentGridNode.scrollView.contentOffset.y - self.contentGridNode.scrollView.contentOffset.y + + let gridSize = CGSize(width: size.width, height: size.height - 5.0) + self.recentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.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.animatePositionAdditive(node: self.recentGridNode, offset: offset) + } + } + + private func transitionToContentGridLayout(_ transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)) { + if let (size, bottomInset) = self.validLayout { + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + + let offset = self.recentGridNode.scrollView.contentOffset.y - self.contentGridNode.scrollView.contentOffset.y + + let gridSize = CGSize(width: size.width, height: size.height - 5.0) + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.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.animatePositionAdditive(node: self.contentGridNode, offset: -offset) + } + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + let actualTransition = self.overrideGridOffsetTransition ?? transition + self.overrideGridOffsetTransition = nil + + let titleAreaHeight: CGFloat = 64.0 + + let size = self.bounds.size + let rawTitleOffset = -titleAreaHeight - presentationLayout.contentOffset.y + let titleOffset = max(-titleAreaHeight, rawTitleOffset) + + let cancelButtonSize = self.cancelButtonNode.measure(CGSize(width: 320.0, height: 100.0)) + let cancelButtonFrame = CGRect(origin: CGPoint(x: bounds.size.width - cancelButtonSize.width - 12.0, y: titleOffset + 25.0), size: cancelButtonSize) + transition.updateFrame(node: self.cancelButtonNode, frame: cancelButtonFrame) + + let searchNodeFrame = CGRect(origin: CGPoint(x: 16.0, y: titleOffset + 16.0), size: CGSize(width: cancelButtonFrame.minX - 16.0 - 10.0, height: 40.0)) + transition.updateFrame(node: self.searchNode, frame: searchNodeFrame) + self.searchNode.updateLayout(width: searchNodeFrame.size.width, transition: transition) + + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleOffset + titleAreaHeight + 5.0), size: CGSize(width: size.width, height: UIScreenPixel))) + + if rawTitleOffset.isLess(than: -titleAreaHeight) { + self.contentSeparatorNode.alpha = 1.0 + } else { + self.contentSeparatorNode.alpha = 0.0 + } + + self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) + } + + func animateIn() { + } + + func updateSelectedPeers() { + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ShareControllerPeerGridItemNode { + itemNode.updateSelection(animated: true) + } + } + self.recentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ShareControllerPeerGridItemNode { + itemNode.updateSelection(animated: true) + } else if let itemNode = itemNode as? ShareControllerRecentPeersGridItemNode { + itemNode.updateSelection(animated: true) + } + } + } + + @objc func cancelPressed() { + self.cancel?() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let nodes: [ASDisplayNode] = [self.searchNode, self.cancelButtonNode] + for node in nodes { + let nodeFrame = node.frame + if let result = node.hitTest(point.offsetBy(dx: -nodeFrame.minX, dy: -nodeFrame.minY), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } + + private func enqueueTransition(_ transition: ShareSearchGridTransaction, firstTime: Bool) { + self.enqueuedTransitions.append((transition, firstTime)) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var itemTransition: ContainedViewLayoutTransition = .immediate + if transition.animated { + itemTransition = .animated(duration: 0.3, curve: .spring) + } + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } + } + + private func enqueueRecentTransition(_ transition: ShareSearchGridTransaction, firstTime: Bool) { + self.enqueuedRecentTransitions.append((transition, firstTime)) + + if self.validLayout != nil { + while !self.enqueuedRecentTransitions.isEmpty { + self.dequeueRecentTransition() + } + } + } + + private func dequeueRecentTransition() { + if let (transition, firstTime) = self.enqueuedRecentTransitions.first { + self.enqueuedRecentTransitions.remove(at: 0) + + var itemTransition: ContainedViewLayoutTransition = .immediate + if transition.animated { + itemTransition = .animated(duration: 0.3, curve: .spring) + } + self.recentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } + } +} diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index cce0aed21a..5f7342cf31 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -31,7 +31,7 @@ final class StickerPackPreviewController: ViewController { self.statusBar.statusBarStyle = .Ignore - self.stickerPackContents.set(loadedStickerPack(account: account, reference: stickerPack)) + self.stickerPackContents.set(loadedStickerPack(postbox: account.postbox, network: account.network, reference: stickerPack)) } required init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index ee38910aa1..8a2dd3f8d5 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -378,6 +378,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol case let .result(info, items, installed): if installed { let _ = removeStickerPackInteractively(postbox: self.account.postbox, id: info.id).start() + self.cancelButtonPressed() } else { let _ = addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items).start() self.cancelButtonPressed() diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 54ec92a9af..3f81ab809c 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -131,21 +131,19 @@ private enum StorageUsageEntry: ItemListNodeEntry { } } -private func stringForKeepMediaTimeout(_ timeout: Int32) -> String { - if timeout <= 7 * 24 * 60 * 60 { - return "1 week" - } else if timeout <= 1 * 31 * 24 * 60 * 60 { - return "1 month" +private func stringForKeepMediaTimeout(strings: PresentationStrings, timeout: Int32) -> String { + if timeout > 1 * 31 * 24 * 60 * 60 { + return strings.MessageTimer_Forever } else { - return "Forever" + return timeIntervalString(strings: strings, value: timeout) } } private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?) -> [StorageUsageEntry] { var entries: [StorageUsageEntry] = [] - entries.append(.keepMedia(presentationData.theme, "Keep Media", stringForKeepMediaTimeout(cacheSettings.defaultCacheStorageTimeout))) - entries.append(.keepMediaInfo(presentationData.theme, "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.")) + entries.append(.keepMedia(presentationData.theme, presentationData.strings.Cache_KeepMedia, stringForKeepMediaTimeout(strings: presentationData.strings, timeout: cacheSettings.defaultCacheStorageTimeout))) + entries.append(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_Help)) var addedHeader = false @@ -166,7 +164,7 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c if let peer = stats.peers[peerId] { if !addedHeader { addedHeader = true - entries.append(.peersHeader(presentationData.theme, "CHATS")) + entries.append(.peersHeader(presentationData.theme, presentationData.strings.Cache_ByPeerHeader)) } entries.append(.peer(index, presentationData.theme, presentationData.strings, peer, dataSizeString(Int(size)))) index += 1 @@ -174,22 +172,22 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c } } } else { - entries.append(.collecting(presentationData.theme, "Calculating current cache size...")) + entries.append(.collecting(presentationData.theme, presentationData.strings.Cache_Indexing)) } return entries } -private func stringForCategory(_ category: PeerCacheUsageCategory) -> String { +private func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String { switch category { case .image: - return "Photos" + return strings.Cache_Photos case .video: - return "Videos" + return strings.Cache_Videos case .audio: - return "Audio" + return strings.Cache_Music case .file: - return "Documents" + return strings.Cache_Files } } @@ -218,6 +216,7 @@ func storageUsageController(account: Account) -> ViewController { actionDisposables.add(clearDisposable) let arguments = StorageUsageControllerArguments(account: account, updateKeepMedia: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -227,28 +226,27 @@ func storageUsageController(account: Account) -> ViewController { return current.withUpdatedDefaultCacheStorageTimeout(timeout) }).start() } + let values: [Int32] = [ + 7 * 24 * 60 * 60, + 1 * 31 * 24 * 60 * 60, + Int32.max + ] + let timeoutItems: [ActionSheetItem] = values.map { value in + return ActionSheetButtonItem(title: stringForKeepMediaTimeout(strings: presentationData.strings, timeout: value), action: { + dismissAction() + timeoutAction(value) + }) + } controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "1 week", action: { - dismissAction() - timeoutAction(7 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "1 month", action: { - dismissAction() - timeoutAction(1 * 31 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "Forever", action: { - dismissAction() - timeoutAction(Int32.max) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: timeoutItems), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) }, openPeerMedia: { peerId in let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in if let result = result, case let .result(stats) = result { if let categories = stats.media[peerId] { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -264,9 +262,9 @@ func storageUsageController(account: Account) -> ViewController { let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) if filteredSize == 0 { - title = "Clear" + title = presentationData.strings.Cache_ClearNone } else { - title = "Clear (\(dataSizeString(Int(filteredSize))))" + title = presentationData.strings.Cache_Clear("\(dataSizeString(Int(filteredSize)))").0 } if let item = item as? ActionSheetButtonItem { @@ -303,7 +301,7 @@ func storageUsageController(account: Account) -> ViewController { sizeIndex[categoryId] = (true, categorySize) totalSize += categorySize let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(categoryId), label: dataSizeString(Int(categorySize)), value: true, action: { value in + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(Int(categorySize)), value: true, action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 @@ -311,7 +309,7 @@ func storageUsageController(account: Account) -> ViewController { } if !items.isEmpty { - items.append(ActionSheetButtonItem(title: "Clear (\(dataSizeString(Int(totalSize))))", action: { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(Int(totalSize)))").0, action: { if let statsPromise = statsPromise { var clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) //var clearSize: Int64 = 0 @@ -352,7 +350,7 @@ func storageUsageController(account: Account) -> ViewController { controller.setItemGroups([ ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller) } @@ -364,7 +362,7 @@ func storageUsageController(account: Account) -> ViewController { let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, cacheSettingsPromise.get(), statsPromise.get()) |> deliverOnMainQueue |> map { presentationData, cacheSettings, cacheStats -> (ItemListControllerState, (ItemListNodeState, StorageUsageEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Storage Usage"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Cache_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/StoredMessageFromSearchPeer.swift b/TelegramUI/StoredMessageFromSearchPeer.swift index 78524f72c2..6a5b2e6da6 100644 --- a/TelegramUI/StoredMessageFromSearchPeer.swift +++ b/TelegramUI/StoredMessageFromSearchPeer.swift @@ -12,3 +12,21 @@ func storedMessageFromSearchPeer(account: Account, peer: Peer) -> Signal Signal { + return account.postbox.modify { modifier -> Void in + if modifier.getMessage(message.id) == nil { + for (_, peer) in message.peers { + if modifier.getPeer(peer.id) == nil { + updatePeers(modifier: modifier, peers: [peer], update: { previousPeer, updatedPeer in + return updatedPeer + }) + } + } + + let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media) + + let _ = modifier.addMessages([storeMessage], location: .Random) + } + } +} diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index a781e7703a..2aadeb2510 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -57,7 +57,16 @@ public final class TelegramApplicationContext { self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in if let strongSelf = self { - let _ = strongSelf.currentPresentationData.swap(next) + var stringsUpdated = false + let _ = strongSelf.currentPresentationData.modify { current in + if next.strings !== current.strings { + stringsUpdated = true + } + return next + } + if stringsUpdated { + updateLegacyLocalization(strings: next.strings) + } } })) @@ -78,8 +87,8 @@ public final class TelegramApplicationContext { } } -extension Account { - var telegramApplicationContext: TelegramApplicationContext { +public extension Account { + public var telegramApplicationContext: TelegramApplicationContext { return self.applicationContext as! TelegramApplicationContext } } diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 37257a510a..1ceca26944 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -7,7 +7,12 @@ import MtProtoKitDynamic var legacyComponentsApplication: UIApplication! -private let legacyLocalization = TGLocalization(version: 0, code: "en", dict: [:], isActive: true) +private var legacyLocalization = TGLocalization(version: 0, code: "en", dict: [:], isActive: true) + +func updateLegacyLocalization(strings: PresentationStrings) { + legacyLocalization = TGLocalization(version: 0, code: strings.languageCode, dict: strings.dict, isActive: true) +} + private var legacyDocumentsStorePath: String? private var legacyCanOpenUrl: (URL) -> Bool = { _ in return false } private var legacyOpenUrl: (URL) -> Void = { _ in } diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index a7a6e3d798..fbdafcb822 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -123,12 +123,12 @@ final class TextNodeLayout: NSObject { return rects } - func attributeRects(name: String, at index: Int) -> [CGRect]? { + func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { if let attributedString = self.attributedString { var range = NSRange() let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range) if range.length != 0 { - var rects: [CGRect] = [] + var rects: [(CGRect, CGRect)] = [] for line in self.lines { let lineRange = NSIntersectionRange(range, line.range) if lineRange.length != 0 { @@ -141,7 +141,7 @@ final class TextNodeLayout: NSObject { rightOffset = ceil(CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, nil)) } let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) - rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))) + rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)))) } } if !rects.isEmpty { @@ -200,7 +200,15 @@ final class TextNode: ASDisplayNode { func attributeRects(name: String, at index: Int) -> [CGRect]? { if let cachedLayout = self.cachedLayout { - return cachedLayout.attributeRects(name: name, at: index) + return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 } + } else { + return nil + } + } + + func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { + if let cachedLayout = self.cachedLayout { + return cachedLayout.lineAndAttributeRects(name: name, at: index) } else { return nil } diff --git a/TelegramUI/ThemeGalleryItem.swift b/TelegramUI/ThemeGalleryItem.swift index fcf5be9f2b..7c8548072e 100644 --- a/TelegramUI/ThemeGalleryItem.swift +++ b/TelegramUI/ThemeGalleryItem.swift @@ -111,7 +111,7 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -151,7 +151,7 @@ final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in diff --git a/TelegramUI/ThemeGridController.swift b/TelegramUI/ThemeGridController.swift index 12f7aa24e5..6bc692bbc4 100644 --- a/TelegramUI/ThemeGridController.swift +++ b/TelegramUI/ThemeGridController.swift @@ -29,6 +29,10 @@ final class ThemeGridController: TelegramController { self.title = self.presentationData.strings.Wallpaper_Title self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.scrollToTop = { [weak self] in + self?.controllerNode.scrollToTop() + } + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift index d8c776cacd..4235fc8871 100644 --- a/TelegramUI/ThemeGridControllerNode.swift +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -176,4 +176,8 @@ final class ThemeGridControllerNode: ASDisplayNode { self.dequeueTransitions() } } + + func scrollToTop() { + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } } diff --git a/TelegramUI/TransformImageArguments.swift b/TelegramUI/TransformImageArguments.swift new file mode 100644 index 0000000000..4d5aad7a90 --- /dev/null +++ b/TelegramUI/TransformImageArguments.swift @@ -0,0 +1,29 @@ +import Foundation +import UIKit + +public struct TransformImageArguments: Equatable { + public let corners: ImageCorners + + public let imageSize: CGSize + public let boundingSize: CGSize + public let intrinsicInsets: UIEdgeInsets + + public var drawingSize: CGSize { + let cornersExtendedEdges = self.corners.extendedEdges + return CGSize(width: self.boundingSize.width + cornersExtendedEdges.left + cornersExtendedEdges.right + self.intrinsicInsets.left + self.intrinsicInsets.right, height: self.boundingSize.height + cornersExtendedEdges.top + cornersExtendedEdges.bottom + self.intrinsicInsets.top + self.intrinsicInsets.bottom) + } + + public var drawingRect: CGRect { + let cornersExtendedEdges = self.corners.extendedEdges + return CGRect(x: cornersExtendedEdges.left + self.intrinsicInsets.left, y: cornersExtendedEdges.top + self.intrinsicInsets.top, width: self.boundingSize.width, height: self.boundingSize.height); + } + + public var insets: UIEdgeInsets { + let cornersExtendedEdges = self.corners.extendedEdges + return UIEdgeInsets(top: cornersExtendedEdges.top + self.intrinsicInsets.top, left: cornersExtendedEdges.left + self.intrinsicInsets.left, bottom: cornersExtendedEdges.bottom + self.intrinsicInsets.bottom, right: cornersExtendedEdges.right + self.intrinsicInsets.right) + } + + public static func ==(lhs: TransformImageArguments, rhs: TransformImageArguments) -> Bool { + return lhs.imageSize == rhs.imageSize && lhs.boundingSize == rhs.boundingSize && lhs.corners == rhs.corners + } +} diff --git a/TelegramUI/TransformImageNode.swift b/TelegramUI/TransformImageNode.swift index 90a86e45c9..81bdd699c1 100644 --- a/TelegramUI/TransformImageNode.swift +++ b/TelegramUI/TransformImageNode.swift @@ -4,33 +4,6 @@ import SwiftSignalKit import Display import TelegramCore -public struct TransformImageArguments: Equatable { - public let corners: ImageCorners - - public let imageSize: CGSize - public let boundingSize: CGSize - public let intrinsicInsets: UIEdgeInsets - - public var drawingSize: CGSize { - let cornersExtendedEdges = self.corners.extendedEdges - return CGSize(width: self.boundingSize.width + cornersExtendedEdges.left + cornersExtendedEdges.right + self.intrinsicInsets.left + self.intrinsicInsets.right, height: self.boundingSize.height + cornersExtendedEdges.top + cornersExtendedEdges.bottom + self.intrinsicInsets.top + self.intrinsicInsets.bottom) - } - - public var drawingRect: CGRect { - let cornersExtendedEdges = self.corners.extendedEdges - return CGRect(x: cornersExtendedEdges.left + self.intrinsicInsets.left, y: cornersExtendedEdges.top + self.intrinsicInsets.top, width: self.boundingSize.width, height: self.boundingSize.height); - } - - public var insets: UIEdgeInsets { - let cornersExtendedEdges = self.corners.extendedEdges - return UIEdgeInsets(top: cornersExtendedEdges.top + self.intrinsicInsets.top, left: cornersExtendedEdges.left + self.intrinsicInsets.left, bottom: cornersExtendedEdges.bottom + self.intrinsicInsets.bottom, right: cornersExtendedEdges.right + self.intrinsicInsets.right) - } -} - -public func ==(lhs: TransformImageArguments, rhs: TransformImageArguments) -> Bool { - return lhs.imageSize == rhs.imageSize && lhs.boundingSize == rhs.boundingSize && lhs.corners == rhs.corners -} - public class TransformImageNode: ASDisplayNode { public var imageUpdated: (() -> Void)? public var alphaTransitionOnFirstUpdate = false @@ -38,10 +11,21 @@ public class TransformImageNode: ASDisplayNode { private var argumentsPromise = ValuePromise(ignoreRepeated: true) + private var overlayColor: UIColor? + private var overlayNode: ASDisplayNode? + deinit { self.disposable.dispose() } + override public var frame: CGRect { + didSet { + if let overlayNode = self.overlayNode { + overlayNode.frame = self.bounds + } + } + } + func setSignal(account: Account, signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, dispatchOnDisplayLink: Bool = true) { let argumentsPromise = self.argumentsPromise @@ -63,6 +47,9 @@ public class TransformImageNode: ASDisplayNode { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } strongSelf.contents = next?.cgImage + if let overlayColor = strongSelf.overlayColor { + strongSelf.applyOverlayColor(animated: false) + } if let imageUpdated = strongSelf.imageUpdated { imageUpdated() } @@ -74,6 +61,9 @@ public class TransformImageNode: ASDisplayNode { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } strongSelf.contents = next?.cgImage + if let overlayColor = strongSelf.overlayColor { + strongSelf.applyOverlayColor(animated: false) + } if let imageUpdated = strongSelf.imageUpdated { imageUpdated() } @@ -106,4 +96,49 @@ public class TransformImageNode: ASDisplayNode { } } } + + public func setOverlayColor(_ color: UIColor?, animated: Bool) { + var updated = false + if let overlayColor = self.overlayColor, let color = color { + updated = !overlayColor.isEqual(color) + } else if (self.overlayColor != nil) != (color != nil) { + updated = true + } + if updated { + self.overlayColor = color + if let _ = self.overlayColor { + self.applyOverlayColor(animated: animated) + } else if let overlayNode = self.overlayNode { + self.overlayNode = nil + if animated { + overlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak overlayNode] _ in + overlayNode?.removeFromSupernode() + }) + } else { + overlayNode.removeFromSupernode() + } + } + } + } + + private func applyOverlayColor(animated: Bool) { + if let overlayColor = self.overlayColor { + if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + if let overlayNode = self.overlayNode { + (overlayNode.view as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + overlayNode.tintColor = overlayColor + } else { + let overlayNode = ASDisplayNode(viewBlock: { + return UIImageView() + }, didLoad: nil) + overlayNode.displaysAsynchronously = false + (overlayNode.view as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate) + overlayNode.tintColor = overlayColor + overlayNode.frame = self.bounds + self.addSubnode(overlayNode) + self.overlayNode = overlayNode + } + } + } + } } diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index 4f1fc70d60..e9139e0ab1 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -85,14 +85,14 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode self.addSubnode(self.textNode) } - func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + func updateLayout(_ size: CGSize, transition: ContainedViewLayoutTransition) { let iconSize = self.iconNode.image?.size ?? CGSize() - let textSize = self.textNode.measure(CGSize(width: layout.size.width - 20.0, height: CGFloat.greatestFiniteMagnitude)) + let textSize = self.textNode.measure(CGSize(width: size.width - 20.0, height: CGFloat.greatestFiniteMagnitude)) let spacing: CGFloat = 10.0 let contentHeight = iconSize.height + spacing + textSize.height - let contentVerticalOrigin = floor((layout.size.height - contentHeight) / 2.0) - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: contentVerticalOrigin + iconSize.height + spacing), size: textSize)) + let contentVerticalOrigin = floor((size.height - contentHeight) / 2.0) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: contentVerticalOrigin + iconSize.height + spacing), size: textSize)) } } @@ -169,8 +169,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size)) if let pictureInPictureNode = self.pictureInPictureNode { - transition.updateFrame(node: pictureInPictureNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - pictureInPictureNode.updateLayout(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if let item = self.item { + let placeholderSize = item.content.dimensions.fitted(layout.size) + transition.updateFrame(node: pictureInPictureNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - placeholderSize.width) / 2.0), y: floor((layout.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + pictureInPictureNode.updateLayout(placeholderSize, transition: transition) + } } } @@ -207,6 +210,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } self.videoNode = videoNode + videoNode.isUserInteractionEnabled = false videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335) videoNode.canAttachContent = true self.updateDisplayPlaceholder(!videoNode.ownsContentNode) @@ -254,11 +258,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if displayPlaceholder { if self.pictureInPictureNode == nil { let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.strings) + pictureInPictureNode.isUserInteractionEnabled = false self.pictureInPictureNode = pictureInPictureNode - self.addSubnode(pictureInPictureNode) + self.insertSubnode(pictureInPictureNode, aboveSubnode: self.scrollNode) if let validLayout = self.validLayout { - pictureInPictureNode.frame = CGRect(origin: CGPoint(), size: validLayout.0.size) - pictureInPictureNode.updateLayout(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate) + if let item = self.item { + let placeholderSize = item.content.dimensions.fitted(validLayout.0.size) + pictureInPictureNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.0.size.width - placeholderSize.width) / 2.0), y: floor((validLayout.0.size.height - placeholderSize.height) / 2.0)), size: placeholderSize) + pictureInPictureNode.updateLayout(placeholderSize, transition: .immediate) + } } self.videoNode?.backgroundColor = UIColor(rgb: 0x333335) } @@ -318,6 +326,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + if let pictureInPictureNode = self.pictureInPictureNode { + let transformedPlaceholderFrame = node.view.convert(node.view.bounds, to: pictureInPictureNode.view) + let transform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0) + pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: pictureInPictureNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + pictureInPictureNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: pictureInPictureNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + } } } @@ -338,7 +354,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - self.view.insertSubview(copyView, belowSubview: self.scrollView) + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame let intermediateCompletion = { [weak copyView] in @@ -378,6 +394,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { boundsCompleted = true intermediateCompletion() }) + + if let pictureInPictureNode = self.pictureInPictureNode { + let transformedPlaceholderFrame = node.view.convert(node.view.bounds, to: pictureInPictureNode.view) + let pictureInPictureTransform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0) + pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: pictureInPictureNode.layer.transform), to: NSValue(caTransform3D: pictureInPictureTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + + pictureInPictureNode.layer.animatePosition(from: pictureInPictureNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + pictureInPictureNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { @@ -399,7 +428,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let copyView = node.view.snapshotContentTree()! - //self.view.insertSubview(copyView, belowSubview: self.scrollView) videoNode.isHidden = true copyView.frame = transformedSelfFrame @@ -435,13 +463,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { transformedFrame.origin = CGPoint() - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in boundsCompleted = true intermediateCompletion() }) - //node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let pictureInPictureNode = self.pictureInPictureNode { + pictureInPictureNode.isHidden = true + } + let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in diff --git a/TelegramUI/UniversalVideoContentManager.swift b/TelegramUI/UniversalVideoContentManager.swift index d0e3ad38bb..afa5166941 100644 --- a/TelegramUI/UniversalVideoContentManager.swift +++ b/TelegramUI/UniversalVideoContentManager.swift @@ -171,32 +171,32 @@ final class UniversalVideoContentManager { } } - func statusSignal(id: AnyHashable) -> Signal { + func statusSignal(content: UniversalVideoContent) -> Signal { return Signal { subscriber in var callbacks: UniversalVideoContentHolderCallbacks - if let current = self.holderCallbacks[id] { + if let current = self.holderCallbacks[content.id] { callbacks = current } else { callbacks = UniversalVideoContentHolderCallbacks() - self.holderCallbacks[id] = callbacks + self.holderCallbacks[content.id] = callbacks } let index = callbacks.status.add({ value in subscriber.putNext(value) }) - if let current = self.holders[id] { + if let current = self.holders[content.id] { subscriber.putNext(current.statusValue) } else { - subscriber.putNext(nil) + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(content.duration), timestamp: 0.0, status: .paused)) } return ActionDisposable { Queue.mainQueue().async { - if let current = self.holderCallbacks[id] { + if let current = self.holderCallbacks[content.id] { current.status.remove(index) if current.playbackCompleted.isEmpty { - self.holderCallbacks.removeValue(forKey: id) + self.holderCallbacks.removeValue(forKey: content.id) } } } diff --git a/TelegramUI/UniversalVideoNode.swift b/TelegramUI/UniversalVideoNode.swift index 04d5d8bb69..7781079a82 100644 --- a/TelegramUI/UniversalVideoNode.swift +++ b/TelegramUI/UniversalVideoNode.swift @@ -114,7 +114,7 @@ final class UniversalVideoNode: ASDisplayNode { self?.playbackCompleted?() }) - self._status.set(self.manager.statusSignal(id: self.content.id)) + self._status.set(self.manager.statusSignal(content: self.content)) self.decoration.setStatus(self.status) diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index cb2593529f..ba3c572859 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -11,6 +11,7 @@ private enum ParsedInternalPeerUrlParameter { private enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) + case stickerPack(String) } private enum ParsedUrl { @@ -24,6 +25,7 @@ enum ResolvedUrl { case botStart(peerId: PeerId, payload: String) case groupBotStart(peerId: PeerId, payload: String) case channelMessage(peerId: PeerId, messageId: MessageId) + case stickerPack(name: String) } private func parseInternalUrl(query: String) -> ParsedInternalUrl? { @@ -50,7 +52,9 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { } return .peerName(peerName, nil) } else if pathComponents.count == 2 { - if let value = Int(pathComponents[1]) { + if pathComponents[0] == "addstickers" { + return .stickerPack(pathComponents[1]) + } else if let value = Int(pathComponents[1]) { return .peerName(peerName, .channelMessage(Int32(value))) } else { return nil @@ -86,17 +90,19 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig return nil } } + case let .stickerPack(name): + return .single(.stickerPack(name: name)) } } func resolveUrl(account: Account, url: String) -> Signal { - let schemes = ["http://", "https://"] + let schemes = ["http://", "https://", ""] let basePaths = ["telegram.me", "t.me"] for basePath in basePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" - if url.hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: url.substring(from: basePrefix.endIndex)) { + if url.lowercased().hasPrefix(basePrefix) { + if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])) { return resolveInternalUrl(account: account, url: internalUrl) |> map { resolved -> ResolvedUrl in if let resolved = resolved { diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index aedecda1f3..0bbb5ca8d2 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -11,26 +11,30 @@ private final class UserInfoControllerArguments { let tapAvatarAction: () -> Void let openChat: () -> Void let changeNotificationMuteSettings: () -> Void + let changeNotificationSoundSettings: () -> Void let openSharedMedia: () -> Void let openGroupsInCommon: () -> Void let updatePeerBlocked: (Bool) -> Void let deleteContact: () -> Void let displayUsernameContextMenu: (String) -> Void let call: () -> Void + let openCallMenu: (String) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, call: @escaping () -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName self.tapAvatarAction = tapAvatarAction self.openChat = openChat self.changeNotificationMuteSettings = changeNotificationMuteSettings + self.changeNotificationSoundSettings = changeNotificationSoundSettings self.openSharedMedia = openSharedMedia self.openGroupsInCommon = openGroupsInCommon self.updatePeerBlocked = updatePeerBlocked self.deleteContact = deleteContact self.displayUsernameContextMenu = displayUsernameContextMenu self.call = call + self.openCallMenu = openCallMenu } } @@ -243,7 +247,7 @@ private enum UserInfoEntry: ItemListNodeEntry { return ItemListTextWithLabelItem(theme: theme, label: text, text: value, multiline: true, sectionId: self.section, action: nil) case let .phoneNumber(theme, _, value): return ItemListTextWithLabelItem(theme: theme, label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section, action: { - + arguments.openCallMenu(value.number) }) case let .userName(theme, text, value): return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", multiline: false, sectionId: self.section, action: { @@ -271,6 +275,7 @@ private enum UserInfoEntry: ItemListNodeEntry { }) case let .notificationSound(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + arguments.changeNotificationSoundSettings() }) case let .groupsInCommon(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: "\(value)", sectionId: self.section, style: .plain, action: { @@ -355,7 +360,7 @@ private func stringForBlockAction(strings: PresentationStrings, action: Destruct } } -private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: PostboxCoding?) -> [UserInfoEntry] { +private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: PostboxCoding?, globalNotificationSettings: GlobalNotificationSettings) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { @@ -373,7 +378,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } } - entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: true)) + entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: user.botInfo == nil)) if let cachedUserData = view.cachedData as? CachedUserData { if let about = cachedUserData.about, !about.isEmpty { entries.append(UserInfoEntry.about(presentationData.theme, presentationData.strings.Profile_About, about)) @@ -410,7 +415,12 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } if isEditing { - entries.append(UserInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, "Default")) + var messageSound: PeerMessageSound = .default + if let settings = view.notificationSettings as? TelegramPeerNotificationSettings { + messageSound = settings.messageSound + } + + entries.append(UserInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: messageSound, default: globalNotificationSettings.effective.privateChats.sound))) if view.peerIsContact { entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact), .removeContact)) @@ -462,6 +472,28 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + let cachedAvatarEntries = Atomic?>(value: nil) + + let requestCallImpl: () -> Void = { + let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peerId { + account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in + return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { peer, current in + if let peer = peer, let current = current { + presentControllerImpl?(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) + })]), nil) + } + }) + } + } + } + let arguments = UserInfoControllerArguments(account: account, avatarAndNameInfoContext: avatarAndNameInfoContext, updateEditingName: { editingName in updateState { state in if let _ = state.editingState { @@ -476,7 +508,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll return } - let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in + let galleryController = AvatarGalleryController(account: account, peer: peer, remoteEntries: cachedAvatarEntries.with { $0 }, replaceRootController: { controller, ready in }) hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in @@ -490,6 +522,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, openChat: { openChatImpl?() }, changeNotificationMuteSettings: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -505,32 +538,43 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) } + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, action: { + dismissAction() + notificationAction(0) + })) + let intervals: [Int32] = [ + 1 * 60 * 60, + 8 * 60 * 60, + 2 * 24 * 60 * 60 + ] + for value in intervals { + items.append(ActionSheetButtonItem(title: muteForIntervalString(strings: presentationData.strings, value: value), action: { + dismissAction() + notificationAction(value) + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, action: { + dismissAction() + notificationAction(Int32.max) + })) + controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Enable", action: { - dismissAction() - notificationAction(0) - }), - ActionSheetButtonItem(title: "Mute for 1 hour", action: { - dismissAction() - notificationAction(1 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 8 hours", action: { - dismissAction() - notificationAction(8 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 2 days", action: { - dismissAction() - notificationAction(2 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "Disable", action: { - dismissAction() - notificationAction(Int32.max) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, changeNotificationSoundSettings: { + let _ = (account.postbox.modify { modifier -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (modifier.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (modifier.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } |> deliverOnMainQueue).start(next: { settings in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.0.messageSound, defaultSound: settings.1.effective.privateChats.sound, completion: { sound in + let _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start() + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) }, openSharedMedia: { if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) @@ -544,28 +588,60 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, displayUsernameContextMenu: { text in displayUsernameContextMenuImpl?(text) }, call: { - let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) - if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { - if currentPeerId == peerId { - account.telegramApplicationContext.navigateToCurrentCall?() + requestCallImpl() + }, openCallMenu: { number in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.modify { modifier -> Peer? in + return modifier.getPeer(peerId) + } |> deliverOnMainQueue).start(next: { peer in + if let peer = peer as? TelegramUser, let peerPhoneNumber = peer.phone, formatPhoneNumber(number) == formatPhoneNumber(peerPhoneNumber) { + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: { + dismissAction() + requestCallImpl() + }), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: { + dismissAction() + account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in - return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) - } |> deliverOnMainQueue).start(next: { peer, current in - if let peer = peer, let current = current { - presentControllerImpl?(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { - let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) - })]), nil) - } - }) + account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") } - } + }) }) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)])) - |> map { presentationData, state, view, chatState -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey])) + |> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) + + var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + + if let peer = peer { + let _ = cachedAvatarEntries.modify { value in + if value != nil { + return value + } else { + let promise = Promise<[AvatarGalleryEntry]>() + promise.set(fetchedAvatarGalleryEntries(account: account, peer: peer)) + return promise + } + } + } var leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton if let editingState = state.editingState { @@ -624,12 +700,12 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) - let listState = ItemListNodeState(entries: userInfoEntries(account: account, presentationData: presentationData, view: view, state: state, peerChatState: (chatState.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState), style: .plain) + let listState = ItemListNodeState(entries: userInfoEntries(account: account, presentationData: presentationData, view: view, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() - } + } let controller = ItemListController(account: account, state: signal) diff --git a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index a881da305b..2f709796b4 100644 --- a/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -117,7 +117,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true - self.listView.keepBottomItemOverscrollBackground = true + self.listView.keepBottomItemOverscrollBackground = .white self.listView.limitHitTestToNodes = true self.listView.isHidden = true diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift new file mode 100644 index 0000000000..e33459a2e2 --- /dev/null +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -0,0 +1,215 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +import LegacyComponents + +final class WebEmbedVideoContent: UniversalVideoContent { + let id: AnyHashable + let webpageContent: TelegramMediaWebpageLoadedContent + let dimensions: CGSize + let duration: Int32 + + init?(webpageContent: TelegramMediaWebpageLoadedContent) { + guard let embedUrl = webpageContent.embedUrl else { + return nil + } + self.id = AnyHashable(embedUrl) + self.webpageContent = webpageContent + self.dimensions = webpageContent.embedSize ?? CGSize(width: 128.0, height: 128.0) + self.duration = Int32(webpageContent.duration ?? (0 as Int)) + } + + func makeContentNode(account: Account) -> UniversalVideoContentNode & ASDisplayNode { + return WebEmbedVideoContentNode(account: account, audioSessionManager: account.telegramApplicationContext.mediaManager.audioSession, webpageContent: self.webpageContent) + } +} + +private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { + private let webpageContent: TelegramMediaWebpageLoadedContent + private let intrinsicDimensions: CGSize + + private let playerView: TGEmbedPlayerView + private let audioSessionDisposable = MetaDisposable() + private var hasAudioSession = false + + private let playbackCompletedListeners = Bag<() -> Void>() + + private var initializedStatus = false + private let _status = Promise() + var status: Signal { + return self._status.get() + } + + private let _ready = Promise() + 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, webpageContent: TelegramMediaWebpageLoadedContent) { + self.webpageContent = webpageContent + + let converted = TGWebPageMediaAttachment() + + converted.url = webpageContent.url + converted.displayUrl = webpageContent.displayUrl + converted.pageType = webpageContent.type + converted.siteName = webpageContent.websiteName + converted.title = webpageContent.title + converted.pageDescription = webpageContent.text + converted.embedUrl = webpageContent.embedUrl + converted.embedType = webpageContent.embedType + converted.embedSize = webpageContent.embedSize ?? CGSize() + converted.duration = webpageContent.duration.flatMap { NSNumber.init(value: $0) } ?? 0 + converted.author = webpageContent.author + + if let embedSize = webpageContent.embedSize { + self.intrinsicDimensions = embedSize + } else { + self.intrinsicDimensions = CGSize(width: 480.0, height: 320.0) + } + + var thumbmnailSignal: SSignal? + if let _ = webpageContent.image { + let thumbnail = self.thumbnail + thumbmnailSignal = SSignal(generator: { subscriber in + let disposable = thumbnail.get().start(next: { image in + subscriber?.putNext(image) + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + } + + self.playerView = TGEmbedPlayerView.make(forWebPage: converted, thumbnailSignal: thumbmnailSignal)! + self.playerView.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + self.playerView.disallowPIP = true + self.playerView.isUserInteractionEnabled = false + self.playerView.disallowAutoplay = true + self.playerView.disableControls = true + + super.init() + + self.view.addSubview(self.playerView) + self.playerView.setup(withEmbedSize: self.intrinsicDimensions) + + 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 = webpageContent.image { + self.thumbnailDisposable = (rawMessagePhoto(account: account, photo: image) |> deliverOnMainQueue).start(next: { [weak self] image in + if let strongSelf = self { + strongSelf.thumbnail.set(.single(image)) + strongSelf._ready.set(.single(Void())) + } + }) + } else { + self._ready.set(.single(Void())) + } + + let stateSignal = self.playerView.stateSignal()! + self._status.set(Signal { subscriber in + let innerDisposable = stateSignal.start(next: { next in + if let next = next as? TGEmbedPlayerState { + let status: MediaPlayerPlaybackStatus + if next.playing { + status = .playing + } else if next.downloadProgress.isEqual(to: 1.0) { + status = .buffering(whilePlaying: next.playing) + } else { + status = .paused + } + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, status: status)) + } + }) + return ActionDisposable { + innerDisposable?.dispose() + } + }) + + //self._status.set(self.player.status) + } + + deinit { + self.audioSessionDisposable.dispose() + + self.loadProgressDisposable?.dispose() + self.thumbnailDisposable?.dispose() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.playerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.playerView.transform = CGAffineTransform(scaleX: size.width / self.intrinsicDimensions.width, y: size.height / self.intrinsicDimensions.height) + + //self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + //self.playerNode.frame = CGRect(origin: CGPoint(), size: size) + } + + func play() { + assert(Queue.mainQueue().isCurrent()) + self.playerView.playVideo() + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.playerView.pauseVideo() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + if let state = self.playerView.state, state.playing { + self.pause() + } else { + self.play() + } + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + /*if value { + self.player.playOnceWithSound() + } else { + self.player.continuePlayingWithoutSound() + }*/ + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.playerView.seek(toPosition: timestamp) + } + + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { + return self.playbackCompletedListeners.add(f) + } + + func removePlaybackCompleted(_ index: Int) { + self.playbackCompletedListeners.remove(index) + } +} diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index e7ec7fb5a7..c0dbb39b6b 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -3,7 +3,7 @@ import Display import AsyncDisplayKit class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { - let scrollView: UIScrollView + let scrollNode: ASScrollNode private var containerLayout: ContainerViewLayout? @@ -15,35 +15,35 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { } } if let node = self.zoomableContent?.1 { - self.scrollView.addSubview(node.view) + self.scrollNode.addSubnode(node) } self.resetScrollViewContents() } } override init() { - self.scrollView = UIScrollView() + self.scrollNode = ASScrollNode() if #available(iOSApplicationExtension 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollNode.view.contentInsetAdjustmentBehavior = .never } super.init() - self.scrollView.delegate = self - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.clipsToBounds = false - self.scrollView.scrollsToTop = false - self.scrollView.delaysContentTouches = false + self.scrollNode.view.delegate = self + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delaysContentTouches = false let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.contentTap(_:))) tapRecognizer.tapActionAtPoint = { _ in return .waitForDoubleTap } - self.scrollView.addGestureRecognizer(tapRecognizer) + self.scrollNode.view.addGestureRecognizer(tapRecognizer) - self.view.addSubview(self.scrollView) + self.addSubnode(self.scrollNode) } @objc func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @@ -53,11 +53,11 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { case .tap: self.toggleControlsVisibility() case .doubleTap: - if let contentView = self.zoomableContent?.1.view, self.scrollView.zoomScale.isLessThanOrEqualTo(self.scrollView.minimumZoomScale) { - let pointInView = self.scrollView.convert(location, to: contentView) + if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) { + let pointInView = self.scrollNode.view.convert(location, to: contentView) - let newZoomScale = self.scrollView.maximumZoomScale - let scrollViewSize = self.scrollView.bounds.size + let newZoomScale = self.scrollNode.view.maximumZoomScale + let scrollViewSize = self.scrollNode.view.bounds.size let w = scrollViewSize.width / newZoomScale let h = scrollViewSize.height / newZoomScale @@ -66,9 +66,9 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h) - self.scrollView.zoom(to: rectToZoomTo, animated: true) + self.scrollNode.view.zoom(to: rectToZoomTo, animated: true) } else { - self.scrollView.setZoomScale(self.scrollView.minimumZoomScale, animated: true) + self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true) } default: break @@ -89,7 +89,7 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { self.containerLayout = layout if shouldResetContents { - self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size) + self.scrollNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.resetScrollViewContents() } } @@ -99,18 +99,18 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { return } - self.scrollView.minimumZoomScale = 1.0 - self.scrollView.maximumZoomScale = 1.0 + self.scrollNode.view.minimumZoomScale = 1.0 + self.scrollNode.view.maximumZoomScale = 1.0 //self.scrollView.normalZoomScale = 1.0 - self.scrollView.zoomScale = 1.0 - self.scrollView.contentSize = contentSize + self.scrollNode.view.zoomScale = 1.0 + self.scrollNode.view.contentSize = contentSize contentNode.transform = CATransform3DIdentity contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) self.centerScrollViewContents() - self.scrollView.zoomScale = self.scrollView.minimumZoomScale + self.scrollNode.view.zoomScale = self.scrollNode.view.minimumZoomScale } private func centerScrollViewContents() { @@ -118,7 +118,7 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { return } - let boundsSize = self.scrollView.bounds.size + let boundsSize = self.scrollNode.view.bounds.size if contentSize.width.isLessThanOrEqualTo(0.0) || contentSize.height.isLessThanOrEqualTo(0.0) || boundsSize.width.isLessThanOrEqualTo(0.0) || boundsSize.height.isLessThanOrEqualTo(0.0) { return } @@ -133,16 +133,16 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { maxScale = minScale } - if !self.scrollView.minimumZoomScale.isEqual(to: minScale) { - self.scrollView.minimumZoomScale = minScale + if !self.scrollNode.view.minimumZoomScale.isEqual(to: minScale) { + self.scrollNode.view.minimumZoomScale = minScale } /*if !self.scrollView.normalZoomScale.isEqual(to: minScale) { self.scrollView.normalZoomScale = minScale }*/ - if !self.scrollView.maximumZoomScale.isEqual(to: maxScale) { - self.scrollView.maximumZoomScale = maxScale + if !self.scrollNode.view.maximumZoomScale.isEqual(to: maxScale) { + self.scrollNode.view.maximumZoomScale = maxScale } var contentFrame = contentNode.view.frame