diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1a858d3864..b4cb08cb97 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12963,6 +12963,7 @@ Sorry for the inconvenience."; "Gift.View.Title" = "Gift"; "Gift.View.ReceivedTitle" = "Received Gift"; +"Gift.View.UnavailableTitle" = "Unavailable"; "Gift.View.KeepOrConvertDescription" = "You can keep this gift in your Profile or convert it to %@. [More About Stars >]()"; "Gift.View.KeepOrConvertDescription.Stars_1" = "%@ Star"; "Gift.View.KeepOrConvertDescription.Stars_any" = "%@ Stars"; @@ -12972,20 +12973,36 @@ Sorry for the inconvenience."; "Gift.View.OtherDescription" = "%1$@ can keep this gift in their Profile or convert it to %2$@. [More About Stars >]()"; "Gift.View.OtherDescription.Stars_1" = "%@ Star"; "Gift.View.OtherDescription.Stars_any" = "%@ Stars"; +"Gift.View.UnavailableDescription" = "This gift has sold out"; "Gift.View.From" = "From"; "Gift.View.HiddenName" = "Hidden Name"; +"Gift.View.Send" = "send a gift"; "Gift.View.Date" = "Date"; +"Gift.View.FirstSale" = "First Sale"; +"Gift.View.LastSale" = "Last Sale"; +"Gift.View.Value" = "Value"; +"Gift.View.Sale" = "sale for %@"; +"Gift.View.Sale.Stars_1" = "%@ Star"; +"Gift.View.Sale.Stars_any" = "%@ Stars"; "Gift.View.Availability" = "Availability"; "Gift.View.Availability.Of" = "%1$@ of %2$@"; +"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left"; "Gift.View.Hide" = "Hide from My Page"; "Gift.View.Display" = "Display on My Page"; "Gift.View.Convert" = "Convert to %@"; "Gift.View.Convert.Stars_1" = "%@ Star"; "Gift.View.Convert.Stars_any" = "%@ Stars"; + +"Gift.View.DisplayedInfo" = "The gift is visible on your Page. [View >]()"; +"Gift.View.HiddenInfo" = "This gift is hidden. Only you can see it."; + "Gift.Displayed.Title" = "Gift Saved to Profile"; "Gift.Displayed.Text" = "The gift is now displayed in [your profile]()."; +"Gift.Displayed.NewText" = "The gift is now shown on your Page."; +"Gift.Displayed.View" = "View"; "Gift.Hidden.Title" = "Gift Removed from Profile"; "Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]()."; +"Gift.Hidden.NewText" = "The gift is removed from your Page."; "Gift.Convert.Title" = "Convert Gift to Stars"; "Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift."; "Gift.Convert.Stars_1" = "%@ Star"; @@ -13077,3 +13094,6 @@ Sorry for the inconvenience."; "WebBrowser.AuthChallenge.Title" = "Sign in to %@"; "WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; + +"ChatList.Search.FilterPublicPosts" = "Public Posts"; +"DialogList.SearchSectionPublicPosts" = "Public Posts"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 95b279d4b1..f80d2a96c3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -641,6 +641,7 @@ public enum ChatListSearchFilter: Equatable { case voice case peer(PeerId, Bool, String, String) case date(Int32?, Int32, String) + case publicPosts public var id: Int64 { switch self { @@ -664,6 +665,8 @@ public enum ChatListSearchFilter: Equatable { return 8 case .voice: return 9 + case .publicPosts: + return 10 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 13f76672f3..d67548da26 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -73,8 +73,13 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate url = updatedPath } - let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) - self.webView.load(request) + let updatedUrl = URL(fileURLWithPath: updatedPath) + let request = URLRequest(url: updatedUrl) + if updatedPath.lowercased().hasSuffix(".txt"), let data = try? Data(contentsOf: updatedUrl) { + self.webView.load(data, mimeType: "text/plain", characterEncodingName: "UTF-8", baseURL: URL(string: "http://localhost")!) + } else { + self.webView.load(request) + } } self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 0fbfa73e98..3a51304be1 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -31,6 +31,7 @@ public enum ChatListSearchItemHeaderType { case downloading case recentDownloads case topics + case publicPosts case text(String, AnyHashable) fileprivate func title(strings: PresentationStrings) -> String { @@ -91,6 +92,8 @@ public enum ChatListSearchItemHeaderType { return strings.DownloadList_DownloadedHeader case .topics: return strings.DialogList_SearchSectionTopics + case .publicPosts: + return strings.DialogList_SearchSectionPublicPosts case let .text(text, _): return text } @@ -154,6 +157,8 @@ public enum ChatListSearchItemHeaderType { return .recentDownloads case .topics: return .topics + case .publicPosts: + return .publicPosts case let .text(_, id): return .text(id) } @@ -192,6 +197,7 @@ private enum ChatListSearchItemHeaderId: Hashable { case downloading case recentDownloads case topics + case publicPosts case text(AnyHashable) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index b5168b166d..cac9c67636 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -60,8 +60,9 @@ final class ChatListSearchInteraction { let dismissInput: () -> Void let getSelectedMessageIds: () -> Set? let openStories: ((PeerId, ASDisplayNode) -> Void)? + let switchToFilter: (ChatListSearchPaneKey) -> Void - init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?) { + init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage @@ -76,6 +77,7 @@ final class ChatListSearchInteraction { self.dismissInput = dismissInput self.getSelectedMessageIds = getSelectedMessageIds self.openStories = openStories + self.switchToFilter = switchToFilter } } @@ -120,6 +122,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() private var forumPeer: EnginePeer? + private var hasPublicPostsTab = false + private var showPublicPostsTab = false private var shareStatusDisposable: MetaDisposable? @@ -281,53 +285,27 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo avatarNode: sourceNode as? AvatarNode, sharedProgressDisposable: self.sharedOpenStoryDisposable ) + }, switchToFilter: { [weak self] filter in + guard let self else { + return + } + if filter == .publicPosts && !self.showPublicPostsTab { + self.showPublicPostsTab = true + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + Queue.mainQueue().justDispatch { + self.paneContainerNode.requestSelectPane(filter) + } }) self.paneContainerNode.interaction = interaction self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in - if let strongSelf = self, let key = key { - var filterKey: ChatListSearchFilter - switch key { - case .chats: - filterKey = .chats - case .topics: - filterKey = .topics - case .channels: - filterKey = .channels - case .apps: - filterKey = .apps - case .media: - filterKey = .media - case .downloads: - filterKey = .downloads - case .links: - filterKey = .links - case .files: - filterKey = .files - case .music: - filterKey = .music - case .voice: - filterKey = .voice - } - strongSelf.selectedFilter = .filter(filterKey) - strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter)) - strongSelf.transitionFraction = transitionFraction - - if let (layout, _) = strongSelf.validLayout { - let filters: [ChatListSearchFilter] - if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { - filters = suggestedFilters - } else { - var isForum = false - if case .forum = strongSelf.location { - isForum = true - } - - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && strongSelf.hasDownloads).map(\.filter) - } - strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) - } + guard let self, let key else { + return } + self.currentPaneUpdated(key, transitionFraction: transitionFraction, transition: transition) } self.paneContainerNode.requesDismissInput = { @@ -368,6 +346,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .music case .voice: key = .voice + case .publicPosts: + key = .publicPosts case let .date(minDate, maxDate, title): date = (minDate, maxDate, title) case let .peer(id, isGroup, _, compactDisplayTitle): @@ -435,7 +415,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return (.complete() |> delay(0.25, queue: Queue.mainQueue())) |> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))) } - } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in + } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in var suggestedFilters: [ChatListSearchFilter] = [] if !dates.isEmpty { let formatter = DateFormatter() @@ -481,26 +461,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo existingPeerIds.insert(peer.id) } } - return suggestedFilters + return (suggestedFilters, searchQuery?.hasPrefix("#") ?? false) } - |> deliverOnMainQueue).startStrict(next: { [weak self] filters in + |> deliverOnMainQueue).startStrict(next: { [weak self] filters, hasPublicPosts in guard let strongSelf = self else { return } var filteredFilters: [ChatListSearchFilter] = [] - for filter in filters { - if case .date = filter, strongSelf.searchOptionsValue?.date == nil { - filteredFilters.append(filter) - } - if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { - filteredFilters.append(filter) + if !hasPublicPosts { + for filter in filters { + if case .date = filter, strongSelf.searchOptionsValue?.date == nil { + filteredFilters.append(filter) + } + if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { + filteredFilters.append(filter) + } } } let previousFilters = strongSelf.suggestedFilters strongSelf.suggestedFilters = filteredFilters - if filteredFilters != previousFilters { + let previousHasPublicPosts = strongSelf.hasPublicPostsTab + strongSelf.hasPublicPostsTab = hasPublicPosts + if !hasPublicPosts { + strongSelf.showPublicPostsTab = false + } + + if filteredFilters != previousFilters || hasPublicPosts != previousHasPublicPosts { if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } @@ -652,6 +640,52 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat))) } + private func currentPaneUpdated(_ key: ChatListSearchPaneKey, transitionFraction: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + var filterKey: ChatListSearchFilter + switch key { + case .chats: + filterKey = .chats + case .topics: + filterKey = .topics + case .channels: + filterKey = .channels + case .apps: + filterKey = .apps + case .media: + filterKey = .media + case .downloads: + filterKey = .downloads + case .links: + filterKey = .links + case .files: + filterKey = .files + case .music: + filterKey = .music + case .voice: + filterKey = .voice + case .publicPosts: + filterKey = .publicPosts + } + self.selectedFilter = .filter(filterKey) + self.selectedFilterPromise.set(.single(self.selectedFilter)) + self.transitionFraction = transitionFraction + + if let (layout, _) = self.validLayout { + let filters: [ChatListSearchFilter] + if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { + filters = suggestedFilters + } else { + var isForum = false + if case .forum = self.location { + isForum = true + } + + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) + } + self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) + } + } + public func search(filter: ChatListSearchFilter, query: String?) { let key: ChatListSearchPaneKey switch filter { @@ -713,7 +747,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads).map(\.filter) + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) } let overflowInset: CGFloat = 20.0 @@ -891,7 +925,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let availablePanes: [ChatListSearchPaneKey] if self.displaySearchFilters { - availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads) + availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.hasPublicPostsTab) } else { availablePanes = isForum ? [.topics] : [.chats] } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 127b853313..fb1a9bd468 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode { case .voice: title = presentationData.strings.ChatList_Search_FilterVoice icon = nil + case .publicPosts: + title = presentationData.strings.ChatList_Search_FilterPublicPosts + icon = nil case let .peer(peerId, isGroup, displayTitle, _): title = displayTitle let image: UIImage? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 995dcb2fe9..34abc55a86 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -394,6 +394,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case generic case downloading case recentlyDownloaded + case publicPosts } case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) @@ -565,7 +566,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem { + public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ListViewItem { switch self { case let .topic(peer, threadInfo, _, theme, strings, expandType): let actionTitle: String? @@ -876,7 +877,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats, requiresPremiumForMessaging): + case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, section, allPaused, storyStats, requiresPremiumForMessaging): let header: ChatListSearchItemHeader switch orderingKey { case .downloading: @@ -894,11 +895,22 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openClearRecentlyDownloaded() }) case .index: - var headerType: ChatListSearchItemHeaderType = .messages(location: nil) - if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { - headerType = .messages(location: peer.compactDisplayTitle) + if case .publicPosts = section { + if case .publicPosts = key { + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } else { + //TODO:localize + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Show More >", action: { + openPublicPosts() + }) + } + } else { + var headerType: ChatListSearchItemHeaderType = .messages(location: nil) + if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { + headerType = .messages(location: peer.compactDisplayTitle) + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } - header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none var isMedia = false @@ -1034,12 +1046,12 @@ private func chatListSearchContainerPreparedRecentTransition( return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ChatListSearchContainerTransition { 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(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -1373,6 +1385,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .topics: tagMask = nil + case .publicPosts: + tagMask = nil case .channels: tagMask = nil case .apps: @@ -1748,114 +1762,118 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundLocalPeers = .single(([], [:], Set())) } } else if let query = query, (key == .chats || key == .topics) { - let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() - |> map { peers -> [RecentlySearchedPeer] in - let allIds = peers.map(\.peer.peerId) - - let updatedState = previousRecentlySearchedPeersState.modify { current in - if var current = current, current.query == query { - current.ids = current.ids.filter { id in - allIds.contains(id) - } - - return current - } else { - var state = SearchedPeersState() - state.ids = allIds - state.query = query - return state - } - } - - var result: [RecentlySearchedPeer] = [] - if let updatedState = updatedState { - for id in updatedState.ids { - for peer in peers { - if id == peer.peer.peerId { - result.append(peer) + if query.hasPrefix("#") { + foundLocalPeers = .single(([], [:], Set())) + } else { + let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() + |> map { peers -> [RecentlySearchedPeer] in + let allIds = peers.map(\.peer.peerId) + + let updatedState = previousRecentlySearchedPeersState.modify { current in + if var current = current, current.query == query { + current.ids = current.ids.filter { id in + allIds.contains(id) } + + return current + } else { + var state = SearchedPeersState() + state.ids = allIds + state.query = query + return state } } - } - - return result - } - - foundLocalPeers = combineLatest( - context.engine.contacts.searchLocalPeers(query: query.lowercased()), - fixedOrRemovedRecentlySearchedPeers - ) - |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in - let recentlySearched = allRecentlySearched.filter { peer in - guard let peer = peer.peer.peer else { - return false - } - return peer.indexName.matchesByTokens(query) - } - - var peerIds = Set() - - var peers: [EngineRenderedPeer] = [] - for peer in recentlySearched { - if !peerIds.contains(peer.peer.peerId) { - peerIds.insert(peer.peer.peerId) - peers.append(EngineRenderedPeer(peer.peer)) - } - } - for peer in local { - if !peerIds.contains(peer.peerId) { - peerIds.insert(peer.peerId) - peers.append(peer) - } - } - - return context.engine.data.subscribe( - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in - return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) - } - ), - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in - return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) - } - ), - TelegramEngine.EngineData.Item.NotificationSettings.Global() - ) - |> map { notificationSettings, unreadCounts, globalNotificationSettings in - return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) - } - } - |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in - var unread: [EnginePeer.Id: (Int32, Bool)] = [:] - for peer in peers { - var isMuted = false - if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { - if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = peerNotificationSettings.muteState { - if let peer = peer.peer { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: - isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled - } + + var result: [RecentlySearchedPeer] = [] + if let updatedState = updatedState { + for id in updatedState.ids { + for peer in peers { + if id == peer.peer.peerId { + result.append(peer) } } } } - let unreadCount = unreadCounts[peer.peerId] - if let unreadCount = unreadCount, unreadCount > 0 { - unread[peer.peerId] = (Int32(unreadCount), isMuted) + + return result + } + + foundLocalPeers = combineLatest( + context.engine.contacts.searchLocalPeers(query: query.lowercased()), + fixedOrRemovedRecentlySearchedPeers + ) + |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in + let recentlySearched = allRecentlySearched.filter { peer in + guard let peer = peer.peer.peer else { + return false + } + return peer.indexName.matchesByTokens(query) + } + + var peerIds = Set() + + var peers: [EngineRenderedPeer] = [] + for peer in recentlySearched { + if !peerIds.contains(peer.peer.peerId) { + peerIds.insert(peer.peer.peerId) + peers.append(EngineRenderedPeer(peer.peer)) + } + } + for peer in local { + if !peerIds.contains(peer.peerId) { + peerIds.insert(peer.peerId) + peers.append(peer) + } + } + + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { notificationSettings, unreadCounts, globalNotificationSettings in + return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) } } - return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) + |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + var unread: [EnginePeer.Id: (Int32, Bool)] = [:] + for peer in peers { + var isMuted = false + if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { + if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = peerNotificationSettings.muteState { + if let peer = peer.peer { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } + } + } + } + } + let unreadCount = unreadCounts[peer.peerId] + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peer.peerId] = (Int32(unreadCount), isMuted) + } + } + return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) + } } } else if let query = query, key == .channels { foundLocalPeers = combineLatest( @@ -2068,13 +2086,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case .savedMessagesChats = location { foundRemotePeers = .single(([], [], false)) } else if let query = query, case .chats = key { - foundRemotePeers = ( - .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) - |> then( - globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) - |> map { ($0.0, $0.1, false) } + if query.hasPrefix("#") { + foundRemotePeers = .single(([], [], false)) + } else { + foundRemotePeers = ( + .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) + |> then( + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) + |> map { ($0.0, $0.1, false) } + ) ) - ) + } } else if let query = query, case .channels = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) @@ -2133,8 +2155,33 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } + let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError> + if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") { + let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 50) + + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true)) + |> then( + searchSignal + |> map { result -> ([FoundRemoteMessages], Bool) in + let foundMessages = result.0 + let messages: [EngineMessage] + if key == .chats { + messages = foundMessages.messages.prefix(3).map { EngineMessage($0) } + } else { + messages = foundMessages.messages.map { EngineMessage($0) } + } + return ([FoundRemoteMessages(messages: messages, readCounters: foundMessages.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)], false) + } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + } else { + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } + let foundRemoteMessages: Signal<([FoundRemoteMessages], Bool), NoError> - if case .savedMessagesChats = location { + if key == .publicPosts { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if case .savedMessagesChats = location { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) @@ -2146,13 +2193,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in - let limit: Int32 - #if DEBUG - limit = 50 - #else - limit = 50 - #endif - return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: limit) + return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) } let searchSignal = combineLatest(searchSignals) @@ -2294,8 +2335,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundThreads = .single([]) } - return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in + return combineLatest( + accountPeer, + foundLocalPeers, + foundRemotePeers, + foundRemoteMessages, + foundPublicMessages, + presentationDataPromise.get(), + searchStatePromise.get(), + selectionPromise.get(), + resolvedMessage, + fixedRecentlySearchedPeers, + foundThreads + ) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 var entries: [ChatListSearchEntry] = [] var index = 0 @@ -2629,6 +2682,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var firstHeaderId: Int64? if !foundRemotePeers.2 { index = 0 + var existingPostIds = Set() + for foundPublicMessageSet in foundPublicMessages.0 { + for message in foundPublicMessageSet.messages { + if existingPostIds.contains(message.id) { + continue + } + existingPostIds.insert(message.id) + + let headerId = listMessageDateHeaderId(timestamp: message.timestamp) + if firstHeaderId == nil { + firstHeaderId = headerId + } + let peer = EngineRenderedPeer(message: message) + entries.append(.message(message, peer, foundPublicMessageSet.readCounters[message.id.peerId], foundPublicMessageSet.threadsData[message.id]?.info, presentationData, foundPublicMessageSet.totalCount, nil, headerId == firstHeaderId, .index(message.index), nil, .publicPosts, false, nil, false)) + index += 1 + } + } + var existingMessageIds = Set() for foundRemoteMessageSet in foundRemoteMessages.0 { for message in foundRemoteMessageSet.messages { @@ -3131,6 +3202,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) }, openStories: { peerId, avatarNode in strongSelf.interaction.openStories?(peerId, avatarNode) + }, openPublicPosts: { + strongSelf.interaction.switchToFilter(.publicPosts) }) strongSelf.currentEntries = newEntries if strongSelf.key == .downloads { @@ -4663,7 +4736,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .apps, .downloads: + case .chats, .topics, .channels, .apps, .downloads, .publicPosts: let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 28bc9b2f8b..209b5b6819 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -50,6 +50,7 @@ final class ChatListSearchPaneWrapper { public enum ChatListSearchPaneKey { case chats case topics + case publicPosts case channels case apps case media @@ -67,6 +68,8 @@ extension ChatListSearchPaneKey { return .chats case .topics: return .topics + case .publicPosts: + return .publicPosts case .channels: return .channels case .apps: @@ -87,13 +90,16 @@ extension ChatListSearchPaneKey { } } -func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatListSearchPaneKey] { +func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPosts: Bool) -> [ChatListSearchPaneKey] { var result: [ChatListSearchPaneKey] = [] if isForum { result.append(.topics) } else { result.append(.chats) } + if hasPublicPosts { + result.append(.publicPosts) + } result.append(.channels) result.append(.apps) result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift index 0d7dd99769..30453c2122 100644 --- a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -23,6 +23,7 @@ public final class BalancedTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -41,6 +42,7 @@ public final class BalancedTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -58,6 +60,7 @@ public final class BalancedTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -122,6 +125,10 @@ public final class BalancedTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -165,6 +172,7 @@ public final class BalancedTextComponent: Component { self.textView.textShadowBlur = component.textShadowBlur self.textView.textStroke = component.textStroke self.textView.linkHighlightColor = component.highlightColor + self.textView.linkHighlightInset = component.highlightInset self.textView.highlightAttributeAction = component.highlightAction self.textView.tapAttributeAction = component.tapAction self.textView.longTapAttributeAction = component.longTapAction diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 4a1f588656..cfcbaf9b85 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -22,6 +22,7 @@ public final class MultilineTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -39,6 +40,7 @@ public final class MultilineTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -55,6 +57,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -116,6 +119,10 @@ public final class MultilineTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -143,6 +150,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = component.textShadowBlur self.textStroke = component.textStroke self.linkHighlightColor = component.highlightColor + self.linkHighlightInset = component.highlightInset self.highlightAttributeAction = component.highlightAction self.tapAttributeAction = component.tapAction self.longTapAttributeAction = component.longTapAction diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index 7957d6fa9b..8851e6c209 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -277,6 +277,7 @@ open class ImmediateTextView: TextView { private var linkHighlightingNode: LinkHighlightingNode? public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero public var trailingLineWidth: CGFloat? @@ -356,7 +357,7 @@ open class ImmediateTextView: TextView { } } - if let rects = rects { + if var rects, !rects.isEmpty { let linkHighlightingNode: LinkHighlightingNode if let current = strongSelf.linkHighlightingNode { linkHighlightingNode = current @@ -366,7 +367,8 @@ open class ImmediateTextView: TextView { strongSelf.addSubnode(linkHighlightingNode) } linkHighlightingNode.frame = strongSelf.bounds - linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) + linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { strongSelf.linkHighlightingNode = nil linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index dfdd9fbaef..e637ea211b 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -12,12 +12,18 @@ import AnimationCache import MultiAnimationRenderer public final class HashtagSearchController: TelegramBaseController { + public enum Mode: Equatable { + case generic + case noChat + case chatOnly + } + private let queue = Queue() private let context: AccountContext private let peer: EnginePeer? private let query: String - let all: Bool + let mode: Mode let publicPosts: Bool private var transitionDisposable: Disposable? @@ -33,11 +39,11 @@ public final class HashtagSearchController: TelegramBaseController { return self.displayNode as! HashtagSearchControllerNode } - public init(context: AccountContext, peer: EnginePeer?, query: String, all: Bool = false, publicPosts: Bool = false) { + public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false) { self.context = context self.peer = peer self.query = query - self.all = all + self.mode = mode self.publicPosts = publicPosts self.animationCache = context.animationCache diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 3ef278aa27..028c08e53a 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -63,7 +63,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.containerNode = ASDisplayNode() - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil && controller.mode != .chatOnly, cancel: { [weak controller] in controller?.dismiss() }) @@ -75,7 +75,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.recentListNode.alpha = 0.0 let navigationController = controller.navigationController as? NavigationController - if let peer, !controller.all { + if let peer, controller.mode != .noChat { self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil) self.currentController?.alwaysShowSearchResultsAsList = true self.currentController?.showListEmptyResults = true @@ -117,7 +117,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.containerNode) - if controller.all { + if controller.mode == .noChat { self.isSearching.set(self.myChatContents?.searching ?? .single(false)) } else { if let _ = peer { diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m index ec3e9d2888..02bfd423ba 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m @@ -26,7 +26,11 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() if ([UIScreen mainScreen].scale >= 2.0f - FLT_EPSILON) { - if (widescreenWidth >= 932.0f - FLT_EPSILON) + if (widescreenWidth >= 956.0f - FLT_EPSILON) + { + return CGSizeMake(145.0f + TGScreenPixel, 145.0 + TGScreenPixel); + } + else if (widescreenWidth >= 932.0f - FLT_EPSILON) { return CGSizeMake(141.0f + TGScreenPixel, 141.0 + TGScreenPixel); } @@ -38,6 +42,10 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() { return CGSizeMake(137.0f - TGScreenPixel, 137.0f - TGScreenPixel); } + else if (widescreenWidth >= 874.0f - FLT_EPSILON) + { + return CGSizeMake(133.0f - TGScreenPixel, 133.0f - TGScreenPixel); + } else if (widescreenWidth >= 852.0f - FLT_EPSILON) { return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel); diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 52cbe8cf30..fc01db2811 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -222,7 +222,38 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu( + context: AccountContext, + peer: Peer?, + threadTitle: String?, + chatLocation: ChatLocation, + editMediaOptions: LegacyAttachmentMenuMediaEditing?, + addingMedia: Bool, + saveEditedPhotos: Bool, + allowGrouping: Bool, + hasSchedule: Bool, + canSendPolls: Bool, + updatedPresentationData: (initial: PresentationData, signal: Signal), + parentController: LegacyController, + recentlyUsedInlineBots: [Peer], + initialCaption: NSAttributedString, + openGallery: @escaping () -> Void, + openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, + openFileGallery: @escaping () -> Void, + openWebSearch: @escaping () -> Void, + openMap: @escaping () -> Void, + openContacts: @escaping () -> Void, + openPoll: @escaping () -> Void, + presentSelectionLimitExceeded: @escaping () -> Void, + presentCantSendMultipleFiles: @escaping () -> Void, + presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, + sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, + selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, + getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + present: @escaping (ViewController, Any?) -> Void +) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -382,7 +413,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + let galleryTitle: String + if addingMedia { + //TODO:localize + galleryTitle = "Add Photo or Video" + } else { + galleryTitle = editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo + } + let galleryItem = TGMenuSheetButtonItemView(title: galleryTitle, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! @@ -395,11 +433,20 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } } itemViews.append(galleryItem) - underlyingViews.append(galleryItem) + + if addingMedia { + //TODO:localize + let fileItem = TGMenuSheetButtonItemView(title: "Add Document", type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + controller?.dismiss(animated: true) + openFileGallery() + })! + itemViews.append(fileItem) + underlyingViews.append(fileItem) + } } - if !editing { + if !editing && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -408,7 +455,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit underlyingViews.append(fileItem) } - if canEditFile { + if canEditFile && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -488,7 +535,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit itemViews.append(editCurrentItem) } - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openMap() diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 86c841110f..b5968cb66e 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -651,7 +651,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: fileSize(path), attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index 59440be2d5..7b4b0095c4 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -67,14 +67,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { } } if let action = self.action { - let actionColor: UIColor - switch self.actionType { - case .generic: - actionColor = self.theme.chatList.sectionHeaderTextColor - case .destructive: - actionColor = self.theme.list.itemDestructiveColor - } - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: actionColor) + self.updateActionTitle() self.actionButton?.accessibilityLabel = action self.actionButton?.accessibilityTraits = [.button] } @@ -115,16 +108,34 @@ public final class ListSectionHeaderNode: ASDisplayNode { return super.hitTest(point, with: event) } + private func updateActionTitle() { + guard let action = self.action else { + return + } + let actionColor: UIColor + switch self.actionType { + case .generic: + actionColor = self.theme.chatList.sectionHeaderTextColor + case .destructive: + actionColor = self.theme.list.itemDestructiveColor + } + let attributedText = NSMutableAttributedString(string: action, font: actionFont, textColor: actionColor) + if let range = attributedText.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + attributedText.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } + self.actionButtonLabel?.attributedText = attributedText + } + public func updateTheme(theme: PresentationTheme) { if self.theme !== theme { self.theme = theme + + self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor) - - self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor - if let action = self.action { - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) - } + + self.updateActionTitle() if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 50bea98f1a..c6d310bcb7 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -1034,7 +1034,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { _ in return nil }, diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6240ee08ca..6865c28092 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2572,7 +2572,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { footer: AnyComponent(MultilineTextComponent( text: .plain(adsInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index 31db7b01ed..cb130cd736 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -254,6 +254,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, + highlightColor: linkColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 14becffa38..2ffc81d679 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -894,7 +894,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } - dict[-1365150482] = { return Api.StarGift.parse_starGift($0) } + dict[1237678029] = { return Api.StarGift.parse_starGift($0) } dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) } dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) } dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) } diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index a89148bf05..00f070a3ef 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -574,13 +574,13 @@ public extension Api { } public extension Api { enum StarGift: TypeConstructorDescription { - case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64) + case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars): + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): if boxed { - buffer.appendInt32(-1365150482) + buffer.appendInt32(1237678029) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -589,14 +589,16 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityRemains!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)} serializeInt64(convertStars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(firstSaleDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastSaleDate!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars): - return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any)]) + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): + return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any)]) } } @@ -617,6 +619,10 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } var _7: Int64? _7 = reader.readInt64() + var _8: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_8 = reader.readInt32() } + var _9: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_9 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -624,8 +630,10 @@ public extension Api { let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!) + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!, firstSaleDate: _8, lastSaleDate: _9) } else { return nil diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 901664c8fd..dd47bffef6 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 190 + return 191 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index b3041b1984..349401e213 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -34,6 +34,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { case price case convertStars case availability + case soldOut } public struct Availability: Equatable, Codable, PostboxCoding { @@ -61,6 +62,31 @@ public struct StarGift: Equatable, Codable, PostboxCoding { } } + public struct SoldOut: Equatable, Codable, PostboxCoding { + enum CodingKeys: String, CodingKey { + case firstSale + case lastSale + } + + public let firstSale: Int32 + public let lastSale: Int32 + + public init(firstSale: Int32, lastSale: Int32) { + self.firstSale = firstSale + self.lastSale = lastSale + } + + public init(decoder: PostboxDecoder) { + self.firstSale = decoder.decodeInt32ForKey(CodingKeys.firstSale.rawValue, orElse: 0) + self.lastSale = decoder.decodeInt32ForKey(CodingKeys.lastSale.rawValue, orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.firstSale, forKey: CodingKeys.firstSale.rawValue) + encoder.encodeInt32(self.lastSale, forKey: CodingKeys.lastSale.rawValue) + } + } + public enum DecodingError: Error { case generic } @@ -70,13 +96,15 @@ public struct StarGift: Equatable, Codable, PostboxCoding { public let price: Int64 public let convertStars: Int64 public let availability: Availability? + public let soldOut: SoldOut? - public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?) { + public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?) { self.id = id self.file = file self.price = price self.convertStars = convertStars self.availability = availability + self.soldOut = soldOut } public init(from decoder: Decoder) throws { @@ -92,6 +120,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { self.price = try container.decode(Int64.self, forKey: .price) self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0 self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability) + self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut) } public init(decoder: PostboxDecoder) { @@ -100,6 +129,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0) self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0) self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { StarGift.Availability(decoder: $0) }) as? StarGift.Availability + self.soldOut = decoder.decodeObjectForKey(CodingKeys.soldOut.rawValue, decoder: { StarGift.SoldOut(decoder: $0) }) as? StarGift.SoldOut } public func encode(to encoder: Encoder) throws { @@ -114,6 +144,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { try container.encode(self.price, forKey: .price) try container.encode(self.convertStars, forKey: .convertStars) try container.encodeIfPresent(self.availability, forKey: .availability) + try container.encodeIfPresent(self.soldOut, forKey: .soldOut) } public func encode(_ encoder: PostboxEncoder) { @@ -126,21 +157,30 @@ public struct StarGift: Equatable, Codable, PostboxCoding { } else { encoder.encodeNil(forKey: CodingKeys.availability.rawValue) } + if let soldOut = self.soldOut { + encoder.encodeObject(soldOut, forKey: CodingKeys.soldOut.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.soldOut.rawValue) + } } } extension StarGift { init?(apiStarGift: Api.StarGift) { switch apiStarGift { - case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars): + case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale): var availability: Availability? if let availabilityRemains, let availabilityTotal { availability = Availability(remains: availabilityRemains, total: availabilityTotal) } + var soldOut: SoldOut? + if let firstSale, let lastSale { + soldOut = SoldOut(firstSale: firstSale, lastSale: lastSale) + } guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { return nil } - self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability) + self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut) } } } @@ -260,6 +300,7 @@ private final class ProfileGiftsContextImpl { private var count: Int32? private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) + var _state: ProfileGiftsContext.State? private let stateValue = Promise() var state: Signal { return self.stateValue.get() @@ -355,6 +396,7 @@ private final class ProfileGiftsContextImpl { } private func pushState() { + self._state = ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState) self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState))) } } @@ -438,6 +480,14 @@ public final class ProfileGiftsContext { impl.convertStarGift(messageId: messageId) } } + + public var currentState: ProfileGiftsContext.State? { + var state: ProfileGiftsContext.State? + self.impl.syncWith { impl in + state = impl._state + } + return state + } } private extension ProfileGiftsContext.State.StarGift { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 601a067d4b..6bc6301b57 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -131,8 +131,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } + var messageMedia = message.media + if let updatingMedia = itemAttributes.updatingMedia, messageMedia.isEmpty, case let .update(media) = updatingMedia.media { + messageMedia.append(media.media) + } + var isFile = false - inner: for media in message.media { + inner: for media in messageMedia { if let media = media as? TelegramMediaPaidContent { var index = 0 for _ in media.extendedMedia { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index d52d60c761..30bb04bf59 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -106,6 +106,9 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { selectedFile = telegramFile } } + if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile { + selectedFile = file + } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { @@ -135,7 +138,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) + let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile) let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( context: item.context, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index ada3bff052..7d9888408e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -303,7 +303,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } @objc private func progressPressed() { - if let resourceStatus = self.resourceStatus { + if let _ = self.arguments?.attributes.updatingMedia { + if let message = self.message { + self.context?.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } + } + else if let resourceStatus = self.resourceStatus { switch resourceStatus.mediaStatus { case let .fetchStatus(fetchStatus): if let context = self.context, let message = self.message, message.flags.isSending { @@ -590,10 +595,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusUpdated = true } - let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnailImage = !arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil + if case let .update(media) = arguments.attributes.updatingMedia?.media, let file = media.media as? TelegramMediaFile { + hasThumbnail = largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil || file.mimeType.hasPrefix("image/") + hasThumbnailImage = hasThumbnail + } if mediaUpdated { - if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { + if hasThumbnailImage { updateImageSignal = chatMessageImageFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } @@ -1622,59 +1632,64 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - switch resourceStatus.mediaStatus { - case var .fetchStatus(fetchStatus): - if self.message?.forwardInfo != nil { - fetchStatus = resourceStatus.fetchStatus - } - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - - switch fetchStatus { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - var wasCheck = false - if let statusNode = self.statusNode, case .check = statusNode.state { - wasCheck = true + if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media { + let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } else { + switch resourceStatus.mediaStatus { + case var .fetchStatus(fetchStatus): + if self.message?.forwardInfo != nil { + fetchStatus = resourceStatus.fetchStatus } + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - if isAudio && !isVoice && !isSending { - state = .play - } else { - if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { - state = .check(appearance: nil) + switch fetchStatus { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + var wasCheck = false + if let statusNode = self.statusNode, case .check = statusNode.state { + wasCheck = true + } + + if isAudio && !isVoice && !isSending { + state = .play } else { - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { + state = .check(appearance: nil) + } else { + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } + } + case .Local: + if isAudio { + state = .play + } else if let fileIconImage = self.fileIconImage { + state = .customIcon(fileIconImage) + } else { + state = .none + } + case .Remote, .Paused: + if isAudio && !isVoice { + state = .play + } else { + state = .download } } - case .Local: - if isAudio { - state = .play - } else if let fileIconImage = self.fileIconImage { - state = .customIcon(fileIconImage) + case let .playbackStatus(playbackStatus): + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage + + if isViewOnceMessage && playbackStatus == .playing { + state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) + if incoming { + self.consumableContentNode.isHidden = true + } } else { - state = .none - } - case .Remote, .Paused: - if isAudio && !isVoice { - state = .play - } else { - state = .download - } - } - case let .playbackStatus(playbackStatus): - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage - - if isViewOnceMessage && playbackStatus == .playing { - state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) - if incoming { - self.consumableContentNode.isHidden = true - } - } else { - switch playbackStatus { - case .playing: - state = .pause - case .paused: - state = .play + switch playbackStatus { + case .playing: + state = .pause + case .paused: + state = .play + } } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index fda1f35734..bf64620198 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -292,11 +292,9 @@ public final class GiftItemComponent: Component { let buttonColor: UIColor var isStars = false - if component.isSoldOut { - buttonColor = component.theme.list.itemDestructiveColor - } else if component.price.containsEmoji { - buttonColor = UIColor(rgb: 0xd3720a) - isStars = true + if component.price.containsEmoji { + buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) + isStars = !component.isSoldOut } else { buttonColor = component.theme.list.itemAccentColor } @@ -593,7 +591,7 @@ private final class StarsButtonEffectLayer: SimpleLayer { let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage - emitter.birthRate = 25.0 + emitter.birthRate = 14.0 emitter.lifetime = 2.0 emitter.velocity = 12.0 emitter.velocityRange = 3 diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD index 50f4c91908..854b81c822 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/InAppPurchaseManager", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", + "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 869d9a61c0..d5c86b410d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import InAppPurchaseManager import TabSelectorComponent import GiftSetupScreen +import GiftViewScreen import UndoUI final class GiftOptionsScreenComponent: Component { @@ -289,7 +290,19 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } - let isSoldOut = gift.availability?.remains == 0 + var ribbon: GiftItemComponent.Ribbon? + if let _ = gift.soldOut { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_SoldOut, + color: .red + ) + } else if let _ = gift.availability { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_Limited, + color: .blue + ) + } + let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -300,14 +313,9 @@ final class GiftOptionsScreenComponent: Component { theme: environment.theme, peer: nil, subject: .starGift(gift.id, gift.file), - price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", - ribbon: gift.availability != nil ? - GiftItemComponent.Ribbon( - text: environment.strings.Gift_Options_Gift_Limited, - color: .blue - ) - : nil, - isSoldOut: isSoldOut + price: "⭐️ \(gift.price)", + ribbon: ribbon, + isSoldOut: gift.soldOut != nil ) ), effectAlignment: .center, @@ -321,16 +329,11 @@ final class GiftOptionsScreenComponent: Component { mainController = controller } if gift.availability?.remains == 0 { - self.dismissAllTooltips(controller: mainController) - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil), - elevatedLayout: false, - action: { _ in return true } + let giftController = GiftViewScreen( + context: component.context, + subject: .soldOutGift(gift) ) - mainController.present(resultController, in: .window(.root)) - HapticFeedback().error() + mainController.push(giftController) } else { let giftController = GiftSetupScreen( context: component.context, @@ -340,6 +343,7 @@ final class GiftOptionsScreenComponent: Component { ) mainController.push(giftController) } + } } }, @@ -601,7 +605,8 @@ final class GiftOptionsScreenComponent: Component { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: accentColor.withAlphaComponent(0.2), + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -789,7 +794,8 @@ final class GiftOptionsScreenComponent: Component { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: accentColor.withAlphaComponent(0.2), + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 6872f69960..0a22181154 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -35,6 +35,8 @@ private final class GiftViewSheetContent: CombinedComponent { let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void init( context: AccountContext, @@ -43,7 +45,9 @@ private final class GiftViewSheetContent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, - openStarsIntro: @escaping () -> Void + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void ) { self.context = context self.subject = subject @@ -52,6 +56,8 @@ private final class GiftViewSheetContent: CombinedComponent { self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts } static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { @@ -74,6 +80,7 @@ private final class GiftViewSheetContent: CombinedComponent { var cachedCloseImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedSmallChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -134,12 +141,10 @@ private final class GiftViewSheetContent: CombinedComponent { let closeButton = Child(Button.self) let animation = Child(GiftAnimationComponent.self) let title = Child(MultilineTextComponent.self) - let amount = Child(BalancedTextComponent.self) - let amountStar = Child(BundleIconComponent.self) let description = Child(MultilineTextComponent.self) let table = Child(TableComponent.self) + let additionalText = Child(MultilineTextComponent.self) let button = Child(SolidRoundedButtonComponent.self) - let secondaryButton = Child(SolidRoundedButtonComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -154,8 +159,7 @@ private final class GiftViewSheetContent: CombinedComponent { let state = context.state let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let textSideInset: CGFloat = 32.0 + environment.safeInsets.left - + let closeImage: UIImage if let (image, theme) = state.cachedCloseImage, theme === environment.theme { closeImage = image @@ -175,35 +179,41 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) + let titleString: String let animationFile: TelegramMediaFile? let stars: Int64 let convertStars: Int64 let text: String? let entities: [MessageTextEntity]? let limitTotal: Int32? - var outgoing = false var incoming = false var savedToProfile = false var converted = false var giftId: Int64 = 0 - var date: Int32 = 0 - if let arguments = component.subject.arguments { + var date: Int32? + var soldOut = false + if case let .soldOutGift(gift) = component.subject { + animationFile = gift.file + stars = gift.price + text = nil + entities = nil + limitTotal = gift.availability?.total + convertStars = 0 + soldOut = true + titleString = strings.Gift_View_UnavailableTitle + } else if let arguments = component.subject.arguments { animationFile = arguments.gift.file stars = arguments.gift.price text = arguments.text entities = arguments.entities limitTotal = arguments.gift.availability?.total convertStars = arguments.convertStars - if case .message = component.subject { - outgoing = !arguments.incoming - } else { - outgoing = false - } incoming = arguments.incoming || arguments.peerId == component.context.account.peerId savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = arguments.gift.id date = arguments.date + titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title } else { animationFile = nil stars = 0 @@ -211,10 +221,13 @@ private final class GiftViewSheetContent: CombinedComponent { entities = nil limitTotal = nil convertStars = 0 + titleString = "" } var descriptionText: String - if incoming { + if soldOut { + descriptionText = strings.Gift_View_UnavailableDescription + } else if incoming { if !converted { descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string } else { @@ -242,19 +255,11 @@ private final class GiftViewSheetContent: CombinedComponent { } descriptionText = modifiedString } - - var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) - if outgoing { - formattedAmount = "- \(formattedAmount)" - } - let countFont: UIFont = Font.semibold(17.0) - let amountText = formattedAmount - let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title, + string: titleString, font: Font.bold(25.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center @@ -266,27 +271,6 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) - let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor) - let amount = amount.update( - component: BalancedTextComponent( - text: .plain(amountAttributedText), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - - let amountStar = amountStar.update( - component: BundleIconComponent( - name: "Premium/Stars/StarMedium", - tintColor: nil - ), - availableSize: context.availableSize, - transition: .immediate - ) - let tableFont = Font.regular(15.0) let tableBoldFont = Font.semibold(15.0) let tableItalicFont = Font.italic(15.0) @@ -296,13 +280,52 @@ private final class GiftViewSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] - - if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { - tableItems.append(.init( - id: "from", - title: strings.Gift_View_From, - component: AnyComponent( - Button( + + if !soldOut { + if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { + let fromComponent: AnyComponent + if incoming { + fromComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Send, + color: theme.list.itemAccentColor + )), + action: { + component.sendGift(peerId) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + fromComponent = AnyComponent(Button( content: AnyComponent( PeerCellComponent( context: component.context, @@ -312,37 +335,114 @@ private final class GiftViewSheetContent: CombinedComponent { ) ), action: { - if "".isEmpty { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) } + )) + } + tableItems.append(.init( + id: "from", + title: strings.Gift_View_From, + component: fromComponent + )) + } else { + tableItems.append(.init( + id: "from_anon", + title: strings.Gift_View_From, + component: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil + ) ) + )) + } + } + + if case let .soldOutGift(gift) = component.subject, let soldOut = gift.soldOut { + tableItems.append(.init( + id: "firstDate", + title: strings.Gift_View_FirstSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) - } else { + tableItems.append(.init( - id: "from_anon", - title: strings.Gift_View_From, + id: "lastDate", + title: strings.Gift_View_LastSale, component: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil - ) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } else if let date { + tableItems.append(.init( + id: "date", + title: strings.Gift_View_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) } - - tableItems.append(.init( - id: "date", - title: strings.Gift_View_Date, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + + let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let valueComponent: AnyComponent + if incoming && !converted { + valueComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Sale(strings.Gift_View_Sale_Stars(Int32(convertStars))).string, + color: theme.list.itemAccentColor + )), + action: { + component.convertToStars() + } + )) + ) + ], spacing: 4.0) ) + } else { + valueComponent = AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + } + + tableItems.append(.init( + id: "value", + title: strings.Gift_View_Value, + component: valueComponent, + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) )) if let limitTotal { @@ -356,7 +456,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) )) } @@ -390,7 +490,6 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) - let textFont = Font.regular(15.0) let linkColor = theme.actionSheet.controlAccentColor context.add(title @@ -413,15 +512,19 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += animation.size.height } - originY += 69.0 + originY += 80.0 + + if soldOut { + originY -= 12.0 + } - var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - let textColor = theme.list.itemPrimaryTextColor + let textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) + let textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -435,7 +538,8 @@ private final class GiftViewSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 5, lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -450,58 +554,70 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - descriptionSize = description.size - var descriptionOrigin = originY - if "".isEmpty { - descriptionOrigin += amount.size.height + 13.0 - } context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) ) - originY += description.size.height + 10.0 - } else { - originY += 11.0 - } - - let amountSpacing: CGFloat = 1.0 - let totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width - let amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 - - var amountOrigin = originY - if "".isEmpty { - amountOrigin -= descriptionSize.height + 10.0 - if descriptionSize.height > 0 { - originY += amount.size.height + 26.0 - } else { - originY += amount.size.height + 2.0 + originY += description.size.height + 21.0 + if soldOut { + originY -= 7.0 } } else { - originY += amount.size.height + 20.0 + originY += 21.0 } - - let amountLabelOriginX: CGFloat - let amountStarOriginX: CGFloat - if !"".isEmpty { - amountStarOriginX = amountOriginX + amountStar.size.width / 2.0 - amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0 - } else { - amountLabelOriginX = amountOriginX + amount.size.width / 2.0 - amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0 - } - - context.add(amount - .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0)) - ) - context.add(amountStar - .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel)) - ) - + context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) ) originY += table.size.height + 23.0 if incoming && !converted { + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) + } + let descriptionText = savedToProfile ? strings.Gift_View_DisplayedInfo : strings.Gift_View_HiddenInfo + + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + + originY -= 5.0 + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.openMyGifts() + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) + ) + originY += additionalText.size.height + originY += 16.0 + let button = button.update( component: SolidRoundedButtonComponent( title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display, @@ -528,32 +644,6 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += button.size.height originY += 7.0 - - let secondaryButton = secondaryButton.update( - component: SolidRoundedButtonComponent( - title: strings.Gift_View_Convert(strings.Gift_View_Convert_Stars(Int32(convertStars))).string, - theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor), - font: .regular, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - isLoading: false, - action: { - component.convertToStars() - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - let secondaryButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: secondaryButton.size) - context.add(secondaryButton - .position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY)) - ) - originY += secondaryButton.size.height } else { let button = button.update( component: SolidRoundedButtonComponent( @@ -603,6 +693,8 @@ private final class GiftViewSheetComponent: CombinedComponent { let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void init( context: AccountContext, @@ -610,7 +702,9 @@ private final class GiftViewSheetComponent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, - openStarsIntro: @escaping () -> Void + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void ) { self.context = context self.subject = subject @@ -618,6 +712,8 @@ private final class GiftViewSheetComponent: CombinedComponent { self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts } static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { @@ -660,7 +756,9 @@ private final class GiftViewSheetComponent: CombinedComponent { openPeer: context.component.openPeer, updateSavedToProfile: context.component.updateSavedToProfile, convertToStars: context.component.convertToStars, - openStarsIntro: context.component.openStarsIntro + openStarsIntro: context.component.openStarsIntro, + sendGift: context.component.sendGift, + openMyGifts: context.component.openMyGifts )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -730,6 +828,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) + case soldOutGift(StarGift) var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { @@ -739,6 +838,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { } case let .profileGift(peerId, gift): return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + case .soldOutGift: + return nil } return nil } @@ -762,6 +863,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { var updateSavedToProfileImpl: ((Bool) -> Void)? var convertToStarsImpl: (() -> Void)? var openStarsIntroImpl: (() -> Void)? + var sendGiftImpl: ((EnginePeer.Id) -> Void)? + var openMyGiftsImpl: (() -> Void)? super.init( context: context, @@ -779,6 +882,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { }, openStarsIntro: { openStarsIntroImpl?() + }, + sendGift: { peerId in + sendGiftImpl?(peerId) + }, + openMyGifts: { + openMyGiftsImpl?() } ), navigationBarAppearance: .none, @@ -820,20 +929,16 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.dismissAnimated() - let title: String = added ? presentationData.strings.Gift_Displayed_Title : presentationData.strings.Gift_Hidden_Title - var text = added ? presentationData.strings.Gift_Displayed_Text : presentationData.strings.Gift_Hidden_Text - if let _ = updateSavedToProfile { - text = text.replacingOccurrences(of: "]()", with: "").replacingOccurrences(of: "[", with: "") - } - if let navigationController { + let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText + if let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.5) { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: arguments.gift.file, loop: false, title: title, text: text, undoText: nil, customAction: nil), + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { [weak navigationController] action in - if case .info = action, let navigationController { + if case .undo = action, let navigationController { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak navigationController] peer in guard let peer, let navigationController else { @@ -919,6 +1024,40 @@ public class GiftViewScreen: ViewControllerComponentContainer { let introController = context.sharedContext.makeStarsIntroScreen(context: context) self.push(introController) } + sendGiftImpl = { [weak self] peerId in + guard let self else { + return + } + let _ = (context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions) + self.push(controller) + }) + } + openMyGiftsImpl = { [weak self] in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller, animated: true) + } + }) + } } required public init(coder aDecoder: NSCoder) { @@ -1332,3 +1471,98 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + + public init( + context: AccountContext, + text: String, + color: UIColor + ) { + self.context = context + self.text = text + self.color = color + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 6.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor = component.color.withAlphaComponent(0.1) + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 539f4497e5..6843e48c4d 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -936,7 +936,8 @@ final class PeerAllowedReactionsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(paidReactionsFooterText), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 28ee1394a3..785ce6231f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -10,6 +10,7 @@ public enum PeerInfoPaneKey: Int32 { case members case stories case storyArchive + case gifts case media case savedMessagesChats case savedMessages @@ -20,7 +21,6 @@ public enum PeerInfoPaneKey: Int32 { case gifs case groupsInCommon case recommended - case gifts } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index af19ba5bb0..15fb7ee5c1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -149,6 +149,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 0ab7c72463..46f8f53d00 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -13,6 +13,8 @@ import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import PeerInfoChatListPaneNode import PeerInfoChatPaneNode +import TextFormat +import EmojiTextAttachmentView final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey @@ -41,6 +43,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private var iconLayers: [InlineStickerItemLayer] = [] private var isSelected: Bool = false @@ -64,10 +67,46 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { self.pressed() } - func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) { self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) + if !icons.isEmpty { + if self.iconLayers.isEmpty { + for icon in icons { + let iconSize = CGSize(width: 24.0, height: 24.0) + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: icon.fileId.id, + file: icon + ) + + let animationLayer = InlineStickerItemLayer( + context: .account(context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: icon, + cache: context.animationCache, + renderer: context.animationRenderer, + unique: true, + placeholderColor: presentationData.theme.list.mediaPlaceholderColor, + pointSize: iconSize, + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.iconLayers.append(animationLayer) + self.layer.addSublayer(animationLayer) + } + } + } else { + for layer in self.iconLayers { + layer.removeFromSuperlayer() + } + self.iconLayers.removeAll() + } + self.buttonNode.accessibilityLabel = title self.buttonNode.accessibilityTraits = [.button] if isSelected { @@ -76,9 +115,22 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { } func updateLayout(height: CGFloat) -> CGFloat { + var totalWidth: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - return titleSize.width + totalWidth = titleSize.width + + if !self.iconLayers.isEmpty { + totalWidth += 1.0 + let iconSize = CGSize(width: 24.0, height: 24.0) + let spacing: CGFloat = 1.0 + for iconlayer in self.iconLayers { + iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 12.0), size: iconSize) + totalWidth += iconSize.width + spacing + } + totalWidth -= spacing + } + return totalWidth } func updateArea(size: CGSize, sideInset: CGFloat) { @@ -89,6 +141,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { struct PeerInfoPaneSpecifier: Equatable { var key: PeerInfoPaneKey var title: String + var icons: [TelegramMediaFile] } private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { @@ -96,6 +149,7 @@ private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGF } final class PeerInfoPaneTabsContainerNode: ASDisplayNode { + private let context: AccountContext private let scrollNode: ASScrollNode private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] private let selectedLineNode: ASImageNode @@ -104,7 +158,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { var requestSelectPane: ((PeerInfoPaneKey) -> Void)? - override init() { + init(context: AccountContext) { + self.context = context self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() @@ -153,7 +208,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { }) self.paneNodes[specifier.key] = paneNode } - paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) + paneNode.updateText(context: self.context, title: specifier.title, icons: specifier.icons, isSelected: selectedPane == specifier.key, presentationData: presentationData) } var removeKeys: [PeerInfoPaneKey] = [] for (key, _) in self.paneNodes { @@ -598,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode.isUserInteractionEnabled = false - self.tabsContainerNode = PeerInfoPaneTabsContainerNode() + self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context) self.tabsSeparatorNode = ASDisplayNode() self.tabsSeparatorNode.isLayerBacked = true @@ -1122,6 +1177,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String + var icons: [TelegramMediaFile] = [] switch key { case .stories: title = presentationData.strings.PeerInfo_PaneStories @@ -1153,8 +1209,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.PeerInfo_SavedMessagesTabTitle case .gifts: title = presentationData.strings.PeerInfo_PaneGifts + icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).map { $0.gift.file } ?? [] } - return PeerInfoPaneSpecifier(key: key, title: title) + return PeerInfoPaneSpecifier(key: key, title: title, icons: icons) }, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition) for (_, pane) in self.pendingPanes { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e05bcfec47..feed0fde86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -38,7 +38,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private let backgroundNode: ASDisplayNode private let scrollNode: ASScrollNode - private var unlockBackground: ASDisplayNode? + private var unlockBackground: NavigationBackgroundNode? private var unlockSeparator: ASDisplayNode? private var unlockText: ComponentView? private var unlockButton: SolidRoundedButtonNode? @@ -263,7 +263,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.theme = presentationData.theme let unlockText: ComponentView - let unlockBackground: ASDisplayNode + let unlockBackground: NavigationBackgroundNode let unlockSeparator: ASDisplayNode let unlockButton: SolidRoundedButtonNode if let current = self.unlockText { @@ -276,7 +276,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if let current = self.unlockBackground { unlockBackground = current } else { - unlockBackground = ASDisplayNode() + unlockBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor) self.addSubnode(unlockBackground) self.unlockBackground = unlockBackground } @@ -304,7 +304,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } if themeUpdated { - unlockBackground.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + unlockBackground.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) unlockSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) } @@ -326,6 +326,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight)) + unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) let unlockSize = unlockText.update( diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift index bcc480988f..fb4c0c5aa3 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift @@ -192,6 +192,7 @@ public final class ArchiveInfoContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift index 16ed898dd4..b1fdea8c21 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift @@ -189,6 +189,7 @@ public final class BirthdayPickerContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: mainText.string.contains(">") ? UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0) : .zero, highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 20e342ee4d..203a6b76b1 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -616,6 +616,7 @@ final class ChatbotSetupScreenComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.25, highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift index fb4e0e9d03..3bcf7b4b6b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -490,6 +490,8 @@ private final class ParagraphComponent: CombinedComponent { horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index c49448d6af..b341040fc9 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -262,7 +262,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index af129188fe..5dc5f18781 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -933,7 +933,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 5, lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index ee6ff8b47f..e22556c410 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -458,7 +458,8 @@ final class StarsStatisticsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(balanceInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 7b9c5ce639..1a91f61e59 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -222,7 +222,8 @@ private final class SheetContent: CombinedComponent { amountFooter = AnyComponent(MultilineTextComponent( text: .plain(amountInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -232,7 +233,7 @@ private final class SheetContent: CombinedComponent { }, tapAction: { attributes, _ in if let controller = controller() as? StarsWithdrawScreen, let navigationController = controller.navigationController as? NavigationController { - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } } )) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 8805580805..93269196eb 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1390,7 +1390,7 @@ extension ChatControllerImpl { if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).startStandalone(next: { message in - guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { + guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState else { return } var originalMediaReference: AnyMediaReference? @@ -1405,7 +1405,11 @@ extension ChatControllerImpl { } } } - strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference) + var editMediaOptions: MessageMediaEditingOptions? + if case let .media(options) = editMessageState.content { + editMediaOptions = options + } + strongSelf.presentEditingAttachmentMenu(editMediaOptions: editMediaOptions, editMediaReference: originalMediaReference) }) } else { strongSelf.presentAttachmentMenu(subject: .default) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a4a0dff681..12c68336be 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8458,7 +8458,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities) var state = state - if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { + if let editMessageState = state.editMessageState { state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference)) } if !text.isEmpty { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 37d82d35fd..622e6fea0b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -711,7 +711,7 @@ extension ChatControllerImpl { }) } - func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { + func presentEditingAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -814,188 +814,184 @@ extension ChatControllerImpl { hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat } - let controller = legacyAttachmentMenu(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: hasSchedule, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { - self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in + let controller = legacyAttachmentMenu( + context: strongSelf.context, + peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, + threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, + editMediaOptions: menuEditMediaOptions, + addingMedia: editMediaOptions == nil, + saveEditedPhotos: settings.storeEditedPhotos, + allowGrouping: true, + hasSchedule: hasSchedule, + canSendPolls: canSendPolls, + updatedPresentationData: strongSelf.updatedPresentationData, + parentController: legacyController, + recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, + initialCaption: inputText, + openGallery: { + self?.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, silentPosting, scheduleTime in + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + self?.editMessageMediaWithLegacySignals(signals) + }) + }, openCamera: { [weak self] cameraView, menuController in + if let strongSelf = self { + var enablePhoto = true + var enableVideo = true + + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + var storeCapturedPhotos = false + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat + + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + } + + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + if let strongSelf = self { + strongSelf.editMessageMediaWithLegacySignals(signals!) + + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + } + }, recognizedQRCode: { [weak self] code in + if let strongSelf = self { + if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + } + } + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }) + } + }, openFileGallery: { + self?.presentFileMediaPickerOptions(editingMessage: true) + }, openWebSearch: { [weak self] in + self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + }, openMap: { + self?.presentLocationPicker() + }, openContacts: { + self?.presentContactPicker() + }, openPoll: { + if let controller = self?.configurePollCreation() { + self?.effectiveNavigationController?.pushViewController(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentCantSendMultipleFiles: { + guard let strongSelf = self else { + return + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentJpegConversionAlert: { completion in + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { + completion(false) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { + completion(true) + })], actionLayout: .vertical), in: .window(.root)) + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let strongSelf = self else { + completion() + return + } if !inputText.string.isEmpty { strongSelf.clearInputText() } - if editMediaOptions != nil { - self?.editMessageMediaWithLegacySignals(signals) - } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - }) - }, openCamera: { [weak self] cameraView, menuController in - if let strongSelf = self { - var enablePhoto = true - var enableVideo = true - - if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - enableVideo = false - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - } - } - - if bannedSendPhotos != nil { - enablePhoto = false - } - if bannedSendVideos != nil { - enableVideo = false - } - - var storeCapturedPhotos = false - var hasSchedule = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat - - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat - } - - presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in - if let strongSelf = self { - if editMediaOptions != nil { - strongSelf.editMessageMediaWithLegacySignals(signals!) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - } - }, recognizedQRCode: { [weak self] code in - if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { - strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) - } - } - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }) - } - }, openFileGallery: { - self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) - }, openMap: { - self?.presentLocationPicker() - }, openContacts: { - self?.presentContactPicker() - }, openPoll: { - if let controller = self?.configurePollCreation() { - self?.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { - guard let strongSelf = self else { - return - } - let text: String - if slowModeEnabled { - text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached - } else { - text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentCantSendMultipleFiles: { - guard let strongSelf = self else { - return - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentJpegConversionAlert: { completion in - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { - completion(false) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { - completion(true) - })], actionLayout: .vertical), in: .window(.root)) - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - guard let strongSelf = self else { - completion() - return - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { strongSelf.editMessageMediaWithLegacySignals(signals!) completion() - } else { - let immediateCompletion = getAnimatedTransitionSource == nil - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { - if !immediateCompletion { - completion() - } - }) - if immediateCompletion { - completion() - } - } - }, selectRecentlyUsedInlineBot: { [weak self] peer in - if let strongSelf = self, let addressName = peer.addressName { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in - return .text + }, selectRecentlyUsedInlineBot: { [weak self] peer in + if let strongSelf = self, let addressName = peer.addressName { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in + return .text + }) }) - }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + ) controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 08a9f1aab0..4895dcf260 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1530,7 +1530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isEditingMedia = !value.isEmpty isMediaEnabled = !value.isEmpty } else { - isMediaEnabled = false + isMediaEnabled = true } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8213dafa0c..e33af7813a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1925,7 +1925,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController { - return HashtagSearchController(context: context, peer: peer, query: query, all: all) + return HashtagSearchController(context: context, peer: peer, query: query, mode: all ? .noChat : .generic) } public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 093f02443c..b2537ee679 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -209,11 +209,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } } if !skipEntity { + var hashtagValue = hashtag + var peerNameValue: String? + if hashtagValue.contains("@") { + let components = hashtagValue.components(separatedBy: "@") + if components.count == 2, let firstComponent = components.first, let lastComponent = components.last, !firstComponent.isEmpty && !lastComponent.isEmpty { + hashtagValue = firstComponent + peerNameValue = lastComponent + } + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } - string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerNameValue, hashtag: hashtagValue), range: range) } case .BotCommand: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 9ee319ed4f..2cf8b86186 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -788,6 +788,8 @@ public final class WebAppController: ViewController, AttachmentContainable { controller.dismiss() case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { + let forceRequest = json["force_request"] as? Bool ?? false + let _ = forceRequest controller.openUrl("https://t.me\(path)", false, { [weak controller] in let _ = controller // controller?.dismiss()