Various improvements

This commit is contained in:
Ilya Laktyushin 2024-10-09 00:05:41 +04:00
parent 059af7d697
commit 89e3ae02a2
51 changed files with 1305 additions and 660 deletions

View File

@ -12963,6 +12963,7 @@ Sorry for the inconvenience.";
"Gift.View.Title" = "Gift"; "Gift.View.Title" = "Gift";
"Gift.View.ReceivedTitle" = "Received 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" = "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_1" = "%@ Star";
"Gift.View.KeepOrConvertDescription.Stars_any" = "%@ Stars"; "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" = "%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_1" = "%@ Star";
"Gift.View.OtherDescription.Stars_any" = "%@ Stars"; "Gift.View.OtherDescription.Stars_any" = "%@ Stars";
"Gift.View.UnavailableDescription" = "This gift has sold out";
"Gift.View.From" = "From"; "Gift.View.From" = "From";
"Gift.View.HiddenName" = "Hidden Name"; "Gift.View.HiddenName" = "Hidden Name";
"Gift.View.Send" = "send a gift";
"Gift.View.Date" = "Date"; "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" = "Availability";
"Gift.View.Availability.Of" = "%1$@ of %2$@"; "Gift.View.Availability.Of" = "%1$@ of %2$@";
"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left";
"Gift.View.Hide" = "Hide from My Page"; "Gift.View.Hide" = "Hide from My Page";
"Gift.View.Display" = "Display on My Page"; "Gift.View.Display" = "Display on My Page";
"Gift.View.Convert" = "Convert to %@"; "Gift.View.Convert" = "Convert to %@";
"Gift.View.Convert.Stars_1" = "%@ Star"; "Gift.View.Convert.Stars_1" = "%@ Star";
"Gift.View.Convert.Stars_any" = "%@ Stars"; "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.Title" = "Gift Saved to Profile";
"Gift.Displayed.Text" = "The gift is now displayed in [your 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.Title" = "Gift Removed from Profile";
"Gift.Hidden.Text" = "The gift is no longer displayed in [your 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.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.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift.";
"Gift.Convert.Stars_1" = "%@ Star"; "Gift.Convert.Stars_1" = "%@ Star";
@ -13077,3 +13094,6 @@ Sorry for the inconvenience.";
"WebBrowser.AuthChallenge.Title" = "Sign in to %@"; "WebBrowser.AuthChallenge.Title" = "Sign in to %@";
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; "WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";
"ChatList.Search.FilterPublicPosts" = "Public Posts";
"DialogList.SearchSectionPublicPosts" = "Public Posts";

View File

@ -641,6 +641,7 @@ public enum ChatListSearchFilter: Equatable {
case voice case voice
case peer(PeerId, Bool, String, String) case peer(PeerId, Bool, String, String)
case date(Int32?, Int32, String) case date(Int32?, Int32, String)
case publicPosts
public var id: Int64 { public var id: Int64 {
switch self { switch self {
@ -664,6 +665,8 @@ public enum ChatListSearchFilter: Equatable {
return 8 return 8
case .voice: case .voice:
return 9 return 9
case .publicPosts:
return 10
case let .peer(peerId, _, _, _): case let .peer(peerId, _, _, _):
return peerId.id._internalGetInt64Value() return peerId.id._internalGetInt64Value()
case let .date(_, date, _): case let .date(_, date, _):

View File

@ -73,8 +73,13 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
url = updatedPath url = updatedPath
} }
let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) let updatedUrl = URL(fileURLWithPath: updatedPath)
self.webView.load(request) 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) self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document)

View File

@ -31,6 +31,7 @@ public enum ChatListSearchItemHeaderType {
case downloading case downloading
case recentDownloads case recentDownloads
case topics case topics
case publicPosts
case text(String, AnyHashable) case text(String, AnyHashable)
fileprivate func title(strings: PresentationStrings) -> String { fileprivate func title(strings: PresentationStrings) -> String {
@ -91,6 +92,8 @@ public enum ChatListSearchItemHeaderType {
return strings.DownloadList_DownloadedHeader return strings.DownloadList_DownloadedHeader
case .topics: case .topics:
return strings.DialogList_SearchSectionTopics return strings.DialogList_SearchSectionTopics
case .publicPosts:
return strings.DialogList_SearchSectionPublicPosts
case let .text(text, _): case let .text(text, _):
return text return text
} }
@ -154,6 +157,8 @@ public enum ChatListSearchItemHeaderType {
return .recentDownloads return .recentDownloads
case .topics: case .topics:
return .topics return .topics
case .publicPosts:
return .publicPosts
case let .text(_, id): case let .text(_, id):
return .text(id) return .text(id)
} }
@ -192,6 +197,7 @@ private enum ChatListSearchItemHeaderId: Hashable {
case downloading case downloading
case recentDownloads case recentDownloads
case topics case topics
case publicPosts
case text(AnyHashable) case text(AnyHashable)
} }

View File

@ -60,8 +60,9 @@ final class ChatListSearchInteraction {
let dismissInput: () -> Void let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>? let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
let openStories: ((PeerId, ASDisplayNode) -> Void)? 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<EngineMessage.Id>?, 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<EngineMessage.Id>?, openStories: ((PeerId, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void) {
self.openPeer = openPeer self.openPeer = openPeer
self.openDisabledPeer = openDisabledPeer self.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage self.openMessage = openMessage
@ -76,6 +77,7 @@ final class ChatListSearchInteraction {
self.dismissInput = dismissInput self.dismissInput = dismissInput
self.getSelectedMessageIds = getSelectedMessageIds self.getSelectedMessageIds = getSelectedMessageIds
self.openStories = openStories self.openStories = openStories
self.switchToFilter = switchToFilter
} }
} }
@ -120,6 +122,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var suggestedFilters: [ChatListSearchFilter]? private var suggestedFilters: [ChatListSearchFilter]?
private let suggestedFiltersDisposable = MetaDisposable() private let suggestedFiltersDisposable = MetaDisposable()
private var forumPeer: EnginePeer? private var forumPeer: EnginePeer?
private var hasPublicPostsTab = false
private var showPublicPostsTab = false
private var shareStatusDisposable: MetaDisposable? private var shareStatusDisposable: MetaDisposable?
@ -281,53 +285,27 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
avatarNode: sourceNode as? AvatarNode, avatarNode: sourceNode as? AvatarNode,
sharedProgressDisposable: self.sharedOpenStoryDisposable 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.interaction = interaction
self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in
if let strongSelf = self, let key = key { guard let self, let key else {
var filterKey: ChatListSearchFilter return
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)
}
} }
self.currentPaneUpdated(key, transitionFraction: transitionFraction, transition: transition)
} }
self.paneContainerNode.requesDismissInput = { self.paneContainerNode.requesDismissInput = {
@ -368,6 +346,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music key = .music
case .voice: case .voice:
key = .voice key = .voice
case .publicPosts:
key = .publicPosts
case let .date(minDate, maxDate, title): case let .date(minDate, maxDate, title):
date = (minDate, maxDate, title) date = (minDate, maxDate, title)
case let .peer(id, isGroup, _, compactDisplayTitle): case let .peer(id, isGroup, _, compactDisplayTitle):
@ -435,7 +415,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
return (.complete() |> delay(0.25, queue: Queue.mainQueue())) return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))) |> 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] = [] var suggestedFilters: [ChatListSearchFilter] = []
if !dates.isEmpty { if !dates.isEmpty {
let formatter = DateFormatter() let formatter = DateFormatter()
@ -481,26 +461,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
existingPeerIds.insert(peer.id) 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 { guard let strongSelf = self else {
return return
} }
var filteredFilters: [ChatListSearchFilter] = [] var filteredFilters: [ChatListSearchFilter] = []
for filter in filters { if !hasPublicPosts {
if case .date = filter, strongSelf.searchOptionsValue?.date == nil { for filter in filters {
filteredFilters.append(filter) if case .date = filter, strongSelf.searchOptionsValue?.date == nil {
} filteredFilters.append(filter)
if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { }
filteredFilters.append(filter) if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil {
filteredFilters.append(filter)
}
} }
} }
let previousFilters = strongSelf.suggestedFilters let previousFilters = strongSelf.suggestedFilters
strongSelf.suggestedFilters = filteredFilters 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 { if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) 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))) 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?) { public func search(filter: ChatListSearchFilter, query: String?) {
let key: ChatListSearchPaneKey let key: ChatListSearchPaneKey
switch filter { switch filter {
@ -713,7 +747,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters filters = suggestedFilters
} else { } 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 let overflowInset: CGFloat = 20.0
@ -891,7 +925,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let availablePanes: [ChatListSearchPaneKey] let availablePanes: [ChatListSearchPaneKey]
if self.displaySearchFilters { if self.displaySearchFilters {
availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads) availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.hasPublicPostsTab)
} else { } else {
availablePanes = isForum ? [.topics] : [.chats] availablePanes = isForum ? [.topics] : [.chats]
} }

View File

@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode {
case .voice: case .voice:
title = presentationData.strings.ChatList_Search_FilterVoice title = presentationData.strings.ChatList_Search_FilterVoice
icon = nil icon = nil
case .publicPosts:
title = presentationData.strings.ChatList_Search_FilterPublicPosts
icon = nil
case let .peer(peerId, isGroup, displayTitle, _): case let .peer(peerId, isGroup, displayTitle, _):
title = displayTitle title = displayTitle
let image: UIImage? let image: UIImage?

View File

@ -394,6 +394,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case generic case generic
case downloading case downloading
case recentlyDownloaded case recentlyDownloaded
case publicPosts
} }
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) 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 { switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType): case let .topic(peer, threadInfo, _, theme, strings, expandType):
let actionTitle: String? let actionTitle: String?
@ -876,7 +877,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode) 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 let header: ChatListSearchItemHeader
switch orderingKey { switch orderingKey {
case .downloading: case .downloading:
@ -894,11 +895,22 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openClearRecentlyDownloaded() openClearRecentlyDownloaded()
}) })
case .index: case .index:
var headerType: ChatListSearchItemHeaderType = .messages(location: nil) if case .publicPosts = section {
if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { if case .publicPosts = key {
headerType = .messages(location: peer.compactDisplayTitle) 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 let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
var isMedia = false var isMedia = false
@ -1034,12 +1046,12 @@ private func chatListSearchContainerPreparedRecentTransition(
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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), 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) 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 tagMask = nil
case .topics: case .topics:
tagMask = nil tagMask = nil
case .publicPosts:
tagMask = nil
case .channels: case .channels:
tagMask = nil tagMask = nil
case .apps: case .apps:
@ -1748,114 +1762,118 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
foundLocalPeers = .single(([], [:], Set())) foundLocalPeers = .single(([], [:], Set()))
} }
} else if let query = query, (key == .chats || key == .topics) { } else if let query = query, (key == .chats || key == .topics) {
let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() if query.hasPrefix("#") {
|> map { peers -> [RecentlySearchedPeer] in foundLocalPeers = .single(([], [:], Set()))
let allIds = peers.map(\.peer.peerId) } else {
let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers()
let updatedState = previousRecentlySearchedPeersState.modify { current in |> map { peers -> [RecentlySearchedPeer] in
if var current = current, current.query == query { let allIds = peers.map(\.peer.peerId)
current.ids = current.ids.filter { id in
allIds.contains(id) let updatedState = previousRecentlySearchedPeersState.modify { current in
} if var current = current, current.query == query {
current.ids = current.ids.filter { id in
return current allIds.contains(id)
} 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)
} }
return current
} else {
var state = SearchedPeersState()
state.ids = allIds
state.query = query
return state
} }
} }
}
var result: [RecentlySearchedPeer] = []
return result if let updatedState = updatedState {
} for id in updatedState.ids {
for peer in peers {
foundLocalPeers = combineLatest( if id == peer.peer.peerId {
context.engine.contacts.searchLocalPeers(query: query.lowercased()), result.append(peer)
fixedOrRemovedRecentlySearchedPeers
)
|> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional<EnginePeer.NotificationSettings>], [EnginePeer.Id: Int], [EngineRenderedPeer], Set<EnginePeer.Id>, 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<EnginePeer.Id>()
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<EnginePeer.Id>) 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 { return result
unread[peer.peerId] = (Int32(unreadCount), isMuted) }
foundLocalPeers = combineLatest(
context.engine.contacts.searchLocalPeers(query: query.lowercased()),
fixedOrRemovedRecentlySearchedPeers
)
|> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional<EnginePeer.NotificationSettings>], [EnginePeer.Id: Int], [EngineRenderedPeer], Set<EnginePeer.Id>, 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<EnginePeer.Id>()
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<EnginePeer.Id>) 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 { } else if let query = query, key == .channels {
foundLocalPeers = combineLatest( foundLocalPeers = combineLatest(
@ -2068,13 +2086,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case .savedMessagesChats = location { if case .savedMessagesChats = location {
foundRemotePeers = .single(([], [], false)) foundRemotePeers = .single(([], [], false))
} else if let query = query, case .chats = key { } else if let query = query, case .chats = key {
foundRemotePeers = ( if query.hasPrefix("#") {
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) foundRemotePeers = .single(([], [], false))
|> then( } else {
globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) foundRemotePeers = (
|> map { ($0.0, $0.1, false) } .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 { } else if let query = query, case .channels = key {
foundRemotePeers = ( foundRemotePeers = (
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) .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> 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)) foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
} else if peersFilter.contains(.doNotSearchMessages) { } else if peersFilter.contains(.doNotSearchMessages) {
foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) 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 searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in
let limit: Int32 return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
#if DEBUG
limit = 50
#else
limit = 50
#endif
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: limit)
} }
let searchSignal = combineLatest(searchSignals) let searchSignal = combineLatest(searchSignals)
@ -2294,8 +2335,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
foundThreads = .single([]) foundThreads = .single([])
} }
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads) return combineLatest(
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in 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 let isSearching = foundRemotePeers.2 || foundRemoteMessages.1
var entries: [ChatListSearchEntry] = [] var entries: [ChatListSearchEntry] = []
var index = 0 var index = 0
@ -2629,6 +2682,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var firstHeaderId: Int64? var firstHeaderId: Int64?
if !foundRemotePeers.2 { if !foundRemotePeers.2 {
index = 0 index = 0
var existingPostIds = Set<MessageId>()
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<MessageId>() var existingMessageIds = Set<MessageId>()
for foundRemoteMessageSet in foundRemoteMessages.0 { for foundRemoteMessageSet in foundRemoteMessages.0 {
for message in foundRemoteMessageSet.messages { for message in foundRemoteMessageSet.messages {
@ -3131,6 +3202,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}) })
}, openStories: { peerId, avatarNode in }, openStories: { peerId, avatarNode in
strongSelf.interaction.openStories?(peerId, avatarNode) strongSelf.interaction.openStories?(peerId, avatarNode)
}, openPublicPosts: {
strongSelf.interaction.switchToFilter(.publicPosts)
}) })
strongSelf.currentEntries = newEntries strongSelf.currentEntries = newEntries
if strongSelf.key == .downloads { if strongSelf.key == .downloads {
@ -4663,7 +4736,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
switch key { switch key {
case .chats, .topics, .channels, .apps, .downloads: case .chats, .topics, .channels, .apps, .downloads, .publicPosts:
let message = EngineMessage( let message = EngineMessage(
stableId: 0, stableId: 0,
stableVersion: 0, stableVersion: 0,

View File

@ -50,6 +50,7 @@ final class ChatListSearchPaneWrapper {
public enum ChatListSearchPaneKey { public enum ChatListSearchPaneKey {
case chats case chats
case topics case topics
case publicPosts
case channels case channels
case apps case apps
case media case media
@ -67,6 +68,8 @@ extension ChatListSearchPaneKey {
return .chats return .chats
case .topics: case .topics:
return .topics return .topics
case .publicPosts:
return .publicPosts
case .channels: case .channels:
return .channels return .channels
case .apps: 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] = [] var result: [ChatListSearchPaneKey] = []
if isForum { if isForum {
result.append(.topics) result.append(.topics)
} else { } else {
result.append(.chats) result.append(.chats)
} }
if hasPublicPosts {
result.append(.publicPosts)
}
result.append(.channels) result.append(.channels)
result.append(.apps) result.append(.apps)
result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice])

View File

@ -23,6 +23,7 @@ public final class BalancedTextComponent: Component {
public let textShadowBlur: CGFloat? public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor? public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([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, textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil, textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil, highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([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.textShadowBlur = textShadowBlur
self.textStroke = textStroke self.textStroke = textStroke
self.highlightColor = highlightColor self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction self.highlightAction = highlightAction
self.tapAction = tapAction self.tapAction = tapAction
self.longTapAction = longTapAction self.longTapAction = longTapAction
@ -122,6 +125,10 @@ public final class BalancedTextComponent: Component {
return false return false
} }
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true return true
} }
@ -165,6 +172,7 @@ public final class BalancedTextComponent: Component {
self.textView.textShadowBlur = component.textShadowBlur self.textView.textShadowBlur = component.textShadowBlur
self.textView.textStroke = component.textStroke self.textView.textStroke = component.textStroke
self.textView.linkHighlightColor = component.highlightColor self.textView.linkHighlightColor = component.highlightColor
self.textView.linkHighlightInset = component.highlightInset
self.textView.highlightAttributeAction = component.highlightAction self.textView.highlightAttributeAction = component.highlightAction
self.textView.tapAttributeAction = component.tapAction self.textView.tapAttributeAction = component.tapAction
self.textView.longTapAttributeAction = component.longTapAction self.textView.longTapAttributeAction = component.longTapAction

View File

@ -22,6 +22,7 @@ public final class MultilineTextComponent: Component {
public let textShadowBlur: CGFloat? public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor? public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([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, textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil, textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil, highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([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.textShadowBlur = textShadowBlur
self.textStroke = textStroke self.textStroke = textStroke
self.highlightColor = highlightColor self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction self.highlightAction = highlightAction
self.tapAction = tapAction self.tapAction = tapAction
self.longTapAction = longTapAction self.longTapAction = longTapAction
@ -116,6 +119,10 @@ public final class MultilineTextComponent: Component {
return false return false
} }
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true return true
} }
@ -143,6 +150,7 @@ public final class MultilineTextComponent: Component {
self.textShadowBlur = component.textShadowBlur self.textShadowBlur = component.textShadowBlur
self.textStroke = component.textStroke self.textStroke = component.textStroke
self.linkHighlightColor = component.highlightColor self.linkHighlightColor = component.highlightColor
self.linkHighlightInset = component.highlightInset
self.highlightAttributeAction = component.highlightAction self.highlightAttributeAction = component.highlightAction
self.tapAttributeAction = component.tapAction self.tapAttributeAction = component.tapAction
self.longTapAttributeAction = component.longTapAction self.longTapAttributeAction = component.longTapAction

View File

@ -277,6 +277,7 @@ open class ImmediateTextView: TextView {
private var linkHighlightingNode: LinkHighlightingNode? private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor? public var linkHighlightColor: UIColor?
public var linkHighlightInset: UIEdgeInsets = .zero
public var trailingLineWidth: CGFloat? public var trailingLineWidth: CGFloat?
@ -356,7 +357,7 @@ open class ImmediateTextView: TextView {
} }
} }
if let rects = rects { if var rects, !rects.isEmpty {
let linkHighlightingNode: LinkHighlightingNode let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode { if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current linkHighlightingNode = current
@ -366,7 +367,8 @@ open class ImmediateTextView: TextView {
strongSelf.addSubnode(linkHighlightingNode) strongSelf.addSubnode(linkHighlightingNode)
} }
linkHighlightingNode.frame = strongSelf.bounds 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 { } else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in

View File

@ -12,12 +12,18 @@ import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
public final class HashtagSearchController: TelegramBaseController { public final class HashtagSearchController: TelegramBaseController {
public enum Mode: Equatable {
case generic
case noChat
case chatOnly
}
private let queue = Queue() private let queue = Queue()
private let context: AccountContext private let context: AccountContext
private let peer: EnginePeer? private let peer: EnginePeer?
private let query: String private let query: String
let all: Bool let mode: Mode
let publicPosts: Bool let publicPosts: Bool
private var transitionDisposable: Disposable? private var transitionDisposable: Disposable?
@ -33,11 +39,11 @@ public final class HashtagSearchController: TelegramBaseController {
return self.displayNode as! HashtagSearchControllerNode 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.context = context
self.peer = peer self.peer = peer
self.query = query self.query = query
self.all = all self.mode = mode
self.publicPosts = publicPosts self.publicPosts = publicPosts
self.animationCache = context.animationCache self.animationCache = context.animationCache

View File

@ -63,7 +63,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.containerNode = ASDisplayNode() 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() controller?.dismiss()
}) })
@ -75,7 +75,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.recentListNode.alpha = 0.0 self.recentListNode.alpha = 0.0
let navigationController = controller.navigationController as? NavigationController 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 = 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?.alwaysShowSearchResultsAsList = true
self.currentController?.showListEmptyResults = true self.currentController?.showListEmptyResults = true
@ -117,7 +117,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.addSubnode(self.clippingNode) self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.containerNode) self.clippingNode.addSubnode(self.containerNode)
if controller.all { if controller.mode == .noChat {
self.isSearching.set(self.myChatContents?.searching ?? .single(false)) self.isSearching.set(self.myChatContents?.searching ?? .single(false))
} else { } else {
if let _ = peer { if let _ = peer {

View File

@ -26,7 +26,11 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen()
if ([UIScreen mainScreen].scale >= 2.0f - FLT_EPSILON) 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); return CGSizeMake(141.0f + TGScreenPixel, 141.0 + TGScreenPixel);
} }
@ -38,6 +42,10 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen()
{ {
return CGSizeMake(137.0f - TGScreenPixel, 137.0f - TGScreenPixel); 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) else if (widescreenWidth >= 852.0f - FLT_EPSILON)
{ {
return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel); return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel);

View File

@ -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<PresentationData, NoError>), 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<PresentationData, NoError>),
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) let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") 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) 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) controller?.dismiss(animated: true)
openGallery() openGallery()
})! })!
@ -395,11 +433,20 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
} }
} }
itemViews.append(galleryItem) itemViews.append(galleryItem)
underlyingViews.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 let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true) controller?.dismiss(animated: true)
openFileGallery() openFileGallery()
@ -408,7 +455,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
underlyingViews.append(fileItem) underlyingViews.append(fileItem)
} }
if canEditFile { if canEditFile && !addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true) controller?.dismiss(animated: true)
openFileGallery() openFileGallery()
@ -488,7 +535,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
itemViews.append(editCurrentItem) 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 let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true) controller?.dismiss(animated: true)
openMap() openMap()

View File

@ -651,7 +651,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
var randomId: Int64 = 0 var randomId: Int64 = 0
arc4random_buf(&randomId, 8) arc4random_buf(&randomId, 8)
let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) 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] = [] var attributes: [MessageAttribute] = []
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))

View File

@ -67,14 +67,7 @@ public final class ListSectionHeaderNode: ASDisplayNode {
} }
} }
if let action = self.action { if let action = self.action {
let actionColor: UIColor self.updateActionTitle()
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.actionButton?.accessibilityLabel = action self.actionButton?.accessibilityLabel = action
self.actionButton?.accessibilityTraits = [.button] self.actionButton?.accessibilityTraits = [.button]
} }
@ -115,16 +108,34 @@ public final class ListSectionHeaderNode: ASDisplayNode {
return super.hitTest(point, with: event) 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) { public func updateTheme(theme: PresentationTheme) {
if self.theme !== theme { if self.theme !== theme {
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.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor)
self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor self.updateActionTitle()
if let action = self.action {
self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor)
}
if let (size, leftInset, rightInset) = self.validLayout { if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)

View File

@ -1034,7 +1034,8 @@ private final class SheetContent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.1, 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 highlightAction: { _ in
return nil return nil
}, },

View File

@ -2572,7 +2572,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .plain(adsInfoString), text: .plain(adsInfoString),
maximumNumberOfLines: 0, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -254,6 +254,8 @@ private final class SheetContent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -894,7 +894,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) }
dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($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[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) }
dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) } dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) }
dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) } dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) }

View File

@ -574,13 +574,13 @@ public extension Api {
} }
public extension Api { public extension Api {
enum StarGift: TypeConstructorDescription { 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { if boxed {
buffer.appendInt32(-1365150482) buffer.appendInt32(1237678029)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, 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(availabilityRemains!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)}
serializeInt64(convertStars, 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 break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { 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):
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)]) 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() } if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() }
var _7: Int64? var _7: Int64?
_7 = reader.readInt64() _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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -624,8 +630,10 @@ public extension Api {
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
let _c7 = _7 != nil let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil
return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!) 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 { else {
return nil return nil

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization { public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt { public func currentLayer() -> UInt {
return 190 return 191
} }
public func parseMessage(_ data: Data!) -> Any! { public func parseMessage(_ data: Data!) -> Any! {

View File

@ -34,6 +34,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
case price case price
case convertStars case convertStars
case availability case availability
case soldOut
} }
public struct Availability: Equatable, Codable, PostboxCoding { 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 { public enum DecodingError: Error {
case generic case generic
} }
@ -70,13 +96,15 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
public let price: Int64 public let price: Int64
public let convertStars: Int64 public let convertStars: Int64
public let availability: Availability? 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.id = id
self.file = file self.file = file
self.price = price self.price = price
self.convertStars = convertStars self.convertStars = convertStars
self.availability = availability self.availability = availability
self.soldOut = soldOut
} }
public init(from decoder: Decoder) throws { 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.price = try container.decode(Int64.self, forKey: .price)
self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0 self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0
self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability) self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability)
self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut)
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
@ -100,6 +129,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0) self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0)
self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.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.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 { 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.price, forKey: .price)
try container.encode(self.convertStars, forKey: .convertStars) try container.encode(self.convertStars, forKey: .convertStars)
try container.encodeIfPresent(self.availability, forKey: .availability) try container.encodeIfPresent(self.availability, forKey: .availability)
try container.encodeIfPresent(self.soldOut, forKey: .soldOut)
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
@ -126,21 +157,30 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
} else { } else {
encoder.encodeNil(forKey: CodingKeys.availability.rawValue) 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 { extension StarGift {
init?(apiStarGift: Api.StarGift) { init?(apiStarGift: Api.StarGift) {
switch apiStarGift { 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? var availability: Availability?
if let availabilityRemains, let availabilityTotal { if let availabilityRemains, let availabilityTotal {
availability = Availability(remains: availabilityRemains, total: 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 { guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else {
return nil 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 count: Int32?
private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil)
var _state: ProfileGiftsContext.State?
private let stateValue = Promise<ProfileGiftsContext.State>() private let stateValue = Promise<ProfileGiftsContext.State>()
var state: Signal<ProfileGiftsContext.State, NoError> { var state: Signal<ProfileGiftsContext.State, NoError> {
return self.stateValue.get() return self.stateValue.get()
@ -355,6 +396,7 @@ private final class ProfileGiftsContextImpl {
} }
private func pushState() { 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))) 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) 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 { private extension ProfileGiftsContext.State.StarGift {

View File

@ -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 var isFile = false
inner: for media in message.media { inner: for media in messageMedia {
if let media = media as? TelegramMediaPaidContent { if let media = media as? TelegramMediaPaidContent {
var index = 0 var index = 0
for _ in media.extendedMedia { for _ in media.extendedMedia {

View File

@ -106,6 +106,9 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
selectedFile = telegramFile 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) var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { 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( let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments(
context: item.context, context: item.context,

View File

@ -303,7 +303,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
@objc private func progressPressed() { @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 { switch resourceStatus.mediaStatus {
case let .fetchStatus(fetchStatus): case let .fetchStatus(fetchStatus):
if let context = self.context, let message = self.message, message.flags.isSending { if let context = self.context, let message = self.message, message.flags.isSending {
@ -590,10 +595,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusUpdated = true 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 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) 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 { if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media {
case var .fetchStatus(fetchStatus): let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027)
if self.message?.forwardInfo != nil { state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
fetchStatus = resourceStatus.fetchStatus } else {
} switch resourceStatus.mediaStatus {
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
switch fetchStatus { fetchStatus = resourceStatus.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
} }
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
if isAudio && !isVoice && !isSending { switch fetchStatus {
state = .play case let .Fetching(_, progress):
} else { let adjustedProgress = max(progress, 0.027)
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { var wasCheck = false
state = .check(appearance: nil) if let statusNode = self.statusNode, case .check = statusNode.state {
wasCheck = true
}
if isAudio && !isVoice && !isSending {
state = .play
} else { } 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: case let .playbackStatus(playbackStatus):
if isAudio { (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage
state = .play
} else if let fileIconImage = self.fileIconImage { if isViewOnceMessage && playbackStatus == .playing {
state = .customIcon(fileIconImage) 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 { } else {
state = .none switch playbackStatus {
} case .playing:
case .Remote, .Paused: state = .pause
if isAudio && !isVoice { case .paused:
state = .play 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
} }
} }
} }

View File

@ -292,11 +292,9 @@ public final class GiftItemComponent: Component {
let buttonColor: UIColor let buttonColor: UIColor
var isStars = false var isStars = false
if component.isSoldOut { if component.price.containsEmoji {
buttonColor = component.theme.list.itemDestructiveColor buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
} else if component.price.containsEmoji { isStars = !component.isSoldOut
buttonColor = UIColor(rgb: 0xd3720a)
isStars = true
} else { } else {
buttonColor = component.theme.list.itemAccentColor buttonColor = component.theme.list.itemAccentColor
} }
@ -593,7 +591,7 @@ private final class StarsButtonEffectLayer: SimpleLayer {
let emitter = CAEmitterCell() let emitter = CAEmitterCell()
emitter.name = "emitter" emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 25.0 emitter.birthRate = 14.0
emitter.lifetime = 2.0 emitter.lifetime = 2.0
emitter.velocity = 12.0 emitter.velocity = 12.0
emitter.velocityRange = 3 emitter.velocityRange = 3

View File

@ -42,6 +42,7 @@ swift_library(
"//submodules/InAppPurchaseManager", "//submodules/InAppPurchaseManager",
"//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -25,6 +25,7 @@ import GiftItemComponent
import InAppPurchaseManager import InAppPurchaseManager
import TabSelectorComponent import TabSelectorComponent
import GiftSetupScreen import GiftSetupScreen
import GiftViewScreen
import UndoUI import UndoUI
final class GiftOptionsScreenComponent: Component { final class GiftOptionsScreenComponent: Component {
@ -289,7 +290,19 @@ final class GiftOptionsScreenComponent: Component {
self.starsItems[itemId] = visibleItem 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( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -300,14 +313,9 @@ final class GiftOptionsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
peer: nil, peer: nil,
subject: .starGift(gift.id, gift.file), subject: .starGift(gift.id, gift.file),
price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", price: "⭐️ \(gift.price)",
ribbon: gift.availability != nil ? ribbon: ribbon,
GiftItemComponent.Ribbon( isSoldOut: gift.soldOut != nil
text: environment.strings.Gift_Options_Gift_Limited,
color: .blue
)
: nil,
isSoldOut: isSoldOut
) )
), ),
effectAlignment: .center, effectAlignment: .center,
@ -321,16 +329,11 @@ final class GiftOptionsScreenComponent: Component {
mainController = controller mainController = controller
} }
if gift.availability?.remains == 0 { if gift.availability?.remains == 0 {
self.dismissAllTooltips(controller: mainController) let giftController = GiftViewScreen(
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } context: component.context,
let resultController = UndoOverlayController( subject: .soldOutGift(gift)
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 }
) )
mainController.present(resultController, in: .window(.root)) mainController.push(giftController)
HapticFeedback().error()
} else { } else {
let giftController = GiftSetupScreen( let giftController = GiftSetupScreen(
context: component.context, context: component.context,
@ -340,6 +343,7 @@ final class GiftOptionsScreenComponent: Component {
) )
mainController.push(giftController) mainController.push(giftController)
} }
} }
} }
}, },
@ -601,7 +605,8 @@ final class GiftOptionsScreenComponent: Component {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -789,7 +794,8 @@ final class GiftOptionsScreenComponent: Component {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -35,6 +35,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let openMyGifts: () -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -43,7 +45,9 @@ private final class GiftViewSheetContent: CombinedComponent {
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
openMyGifts: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -52,6 +56,8 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.openMyGifts = openMyGifts
} }
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
@ -74,6 +80,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
var inProgress = false var inProgress = false
@ -134,12 +141,10 @@ private final class GiftViewSheetContent: CombinedComponent {
let closeButton = Child(Button.self) let closeButton = Child(Button.self)
let animation = Child(GiftAnimationComponent.self) let animation = Child(GiftAnimationComponent.self)
let title = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self)
let amount = Child(BalancedTextComponent.self)
let amountStar = Child(BundleIconComponent.self)
let description = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self) let table = Child(TableComponent.self)
let additionalText = Child(MultilineTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self)
let secondaryButton = Child(SolidRoundedButtonComponent.self)
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
@ -154,8 +159,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let state = context.state let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let closeImage: UIImage let closeImage: UIImage
if let (image, theme) = state.cachedCloseImage, theme === environment.theme { if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image closeImage = image
@ -175,35 +179,41 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate transition: .immediate
) )
let titleString: String
let animationFile: TelegramMediaFile? let animationFile: TelegramMediaFile?
let stars: Int64 let stars: Int64
let convertStars: Int64 let convertStars: Int64
let text: String? let text: String?
let entities: [MessageTextEntity]? let entities: [MessageTextEntity]?
let limitTotal: Int32? let limitTotal: Int32?
var outgoing = false
var incoming = false var incoming = false
var savedToProfile = false var savedToProfile = false
var converted = false var converted = false
var giftId: Int64 = 0 var giftId: Int64 = 0
var date: Int32 = 0 var date: Int32?
if let arguments = component.subject.arguments { 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 animationFile = arguments.gift.file
stars = arguments.gift.price stars = arguments.gift.price
text = arguments.text text = arguments.text
entities = arguments.entities entities = arguments.entities
limitTotal = arguments.gift.availability?.total limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile savedToProfile = arguments.savedToProfile
converted = arguments.converted converted = arguments.converted
giftId = arguments.gift.id giftId = arguments.gift.id
date = arguments.date date = arguments.date
titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title
} else { } else {
animationFile = nil animationFile = nil
stars = 0 stars = 0
@ -211,10 +221,13 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = nil entities = nil
limitTotal = nil limitTotal = nil
convertStars = 0 convertStars = 0
titleString = ""
} }
var descriptionText: String var descriptionText: String
if incoming { if soldOut {
descriptionText = strings.Gift_View_UnavailableDescription
} else if incoming {
if !converted { if !converted {
descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string
} else { } else {
@ -242,19 +255,11 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
descriptionText = modifiedString 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( let title = title.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title, string: titleString,
font: Font.bold(25.0), font: Font.bold(25.0),
textColor: theme.actionSheet.primaryTextColor, textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center paragraphAlignment: .center
@ -266,27 +271,6 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate 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 tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0) let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0) let tableItalicFont = Font.italic(15.0)
@ -296,13 +280,52 @@ private final class GiftViewSheetContent: CombinedComponent {
let tableTextColor = theme.list.itemPrimaryTextColor let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = [] var tableItems: [TableComponent.Item] = []
if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { if !soldOut {
tableItems.append(.init( if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
id: "from", let fromComponent: AnyComponent<Empty>
title: strings.Gift_View_From, if incoming {
component: AnyComponent( fromComponent = AnyComponent(
Button( 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( content: AnyComponent(
PeerCellComponent( PeerCellComponent(
context: component.context, context: component.context,
@ -312,37 +335,114 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
), ),
action: { action: {
if "".isEmpty { component.openPeer(peer)
component.openPeer(peer) Queue.mainQueue().after(1.0, {
Queue.mainQueue().after(1.0, { component.cancel(false)
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( tableItems.append(.init(
id: "from_anon", id: "lastDate",
title: strings.Gift_View_From, title: strings.Gift_View_LastSale,
component: AnyComponent( component: AnyComponent(
PeerCellComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
context: component.context, )
theme: theme, ))
strings: strings, } else if let date {
peer: nil 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( let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))"
id: "date", let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
title: strings.Gift_View_Date, let range = (valueAttributedString.string as NSString).range(of: "⭐️")
component: AnyComponent( if range.location != NSNotFound {
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) 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<Empty>
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 { if let limitTotal {
@ -356,7 +456,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "availability", id: "availability",
title: strings.Gift_View_Availability, title: strings.Gift_View_Availability,
component: AnyComponent( 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 transition: .immediate
) )
let textFont = Font.regular(15.0)
let linkColor = theme.actionSheet.controlAccentColor let linkColor = theme.actionSheet.controlAccentColor
context.add(title context.add(title
@ -413,15 +512,19 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
originY += animation.size.height originY += animation.size.height
} }
originY += 69.0 originY += 80.0
if soldOut {
originY -= 12.0
}
var descriptionSize: CGSize = .zero
if !descriptionText.isEmpty { if !descriptionText.isEmpty {
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, 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 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) return (TelegramTextAttributes.URL, contents)
}) })
@ -435,7 +538,8 @@ private final class GiftViewSheetContent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 5, maximumNumberOfLines: 5,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return 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), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate transition: .immediate
) )
descriptionSize = description.size
var descriptionOrigin = originY
if "".isEmpty {
descriptionOrigin += amount.size.height + 13.0
}
context.add(description 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 originY += description.size.height + 21.0
} else { if soldOut {
originY += 11.0 originY -= 7.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
} }
} else { } 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 context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
) )
originY += table.size.height + 23.0 originY += table.size.height + 23.0
if incoming && !converted { 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( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display, title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display,
@ -528,32 +644,6 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
originY += button.size.height originY += button.size.height
originY += 7.0 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 { } else {
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
@ -603,6 +693,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let openMyGifts: () -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -610,7 +702,9 @@ private final class GiftViewSheetComponent: CombinedComponent {
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
openMyGifts: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -618,6 +712,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.openMyGifts = openMyGifts
} }
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
@ -660,7 +756,9 @@ private final class GiftViewSheetComponent: CombinedComponent {
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
updateSavedToProfile: context.component.updateSavedToProfile, updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars, 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), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true, followContentSizeChanges: true,
@ -730,6 +828,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable { public enum Subject: Equatable {
case message(EngineMessage) case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) 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)? { 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 { switch self {
@ -739,6 +838,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} }
case let .profileGift(peerId, gift): 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) 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 return nil
} }
@ -762,6 +863,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var updateSavedToProfileImpl: ((Bool) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)? var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)? var openStarsIntroImpl: (() -> Void)?
var sendGiftImpl: ((EnginePeer.Id) -> Void)?
var openMyGiftsImpl: (() -> Void)?
super.init( super.init(
context: context, context: context,
@ -779,6 +882,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}, },
openStarsIntro: { openStarsIntro: {
openStarsIntroImpl?() openStarsIntroImpl?()
},
sendGift: { peerId in
sendGiftImpl?(peerId)
},
openMyGifts: {
openMyGiftsImpl?()
} }
), ),
navigationBarAppearance: .none, navigationBarAppearance: .none,
@ -820,20 +929,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.dismissAnimated() self.dismissAnimated()
let title: String = added ? presentationData.strings.Gift_Displayed_Title : presentationData.strings.Gift_Hidden_Title let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText
var text = added ? presentationData.strings.Gift_Displayed_Text : presentationData.strings.Gift_Hidden_Text if let navigationController = self.navigationController as? NavigationController {
if let _ = updateSavedToProfile {
text = text.replacingOccurrences(of: "]()", with: "").replacingOccurrences(of: "[", with: "")
}
if let navigationController {
Queue.mainQueue().after(0.5) { Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController { if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController( let resultController = UndoOverlayController(
presentationData: presentationData, 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, elevatedLayout: lastController is ChatController,
action: { [weak navigationController] action in 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)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in |> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else { guard let peer, let navigationController else {
@ -919,6 +1024,40 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let introController = context.sharedContext.makeStarsIntroScreen(context: context) let introController = context.sharedContext.makeStarsIntroScreen(context: context)
self.push(introController) 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) { required public init(coder aDecoder: NSCoder) {
@ -1332,3 +1471,98 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
context.strokePath() 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<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -936,7 +936,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .plain(paidReactionsFooterText), text: .plain(paidReactionsFooterText),
maximumNumberOfLines: 0, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL") return NSAttributedString.Key(rawValue: "URL")

View File

@ -10,6 +10,7 @@ public enum PeerInfoPaneKey: Int32 {
case members case members
case stories case stories
case storyArchive case storyArchive
case gifts
case media case media
case savedMessagesChats case savedMessagesChats
case savedMessages case savedMessages
@ -20,7 +21,6 @@ public enum PeerInfoPaneKey: Int32 {
case gifs case gifs
case groupsInCommon case groupsInCommon
case recommended case recommended
case gifts
} }
public struct PeerInfoStatusData: Equatable { public struct PeerInfoStatusData: Equatable {

View File

@ -149,6 +149,7 @@ swift_library(
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/UrlHandling", "//submodules/UrlHandling",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -13,6 +13,8 @@ import PeerInfoVisualMediaPaneNode
import PeerInfoPaneNode import PeerInfoPaneNode
import PeerInfoChatListPaneNode import PeerInfoChatListPaneNode
import PeerInfoChatPaneNode import PeerInfoChatPaneNode
import TextFormat
import EmojiTextAttachmentView
final class PeerInfoPaneWrapper { final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey let key: PeerInfoPaneKey
@ -41,6 +43,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: HighlightTrackingButtonNode
private var iconLayers: [InlineStickerItemLayer] = []
private var isSelected: Bool = false private var isSelected: Bool = false
@ -64,10 +67,46 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
self.pressed() 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.isSelected = isSelected
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) 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.accessibilityLabel = title
self.buttonNode.accessibilityTraits = [.button] self.buttonNode.accessibilityTraits = [.button]
if isSelected { if isSelected {
@ -76,9 +115,22 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
} }
func updateLayout(height: CGFloat) -> CGFloat { func updateLayout(height: CGFloat) -> CGFloat {
var totalWidth: CGFloat = 0.0
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) 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) 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) { func updateArea(size: CGSize, sideInset: CGFloat) {
@ -89,6 +141,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
struct PeerInfoPaneSpecifier: Equatable { struct PeerInfoPaneSpecifier: Equatable {
var key: PeerInfoPaneKey var key: PeerInfoPaneKey
var title: String var title: String
var icons: [TelegramMediaFile]
} }
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { 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 { final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
private let context: AccountContext
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:]
private let selectedLineNode: ASImageNode private let selectedLineNode: ASImageNode
@ -104,7 +158,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
var requestSelectPane: ((PeerInfoPaneKey) -> Void)? var requestSelectPane: ((PeerInfoPaneKey) -> Void)?
override init() { init(context: AccountContext) {
self.context = context
self.scrollNode = ASScrollNode() self.scrollNode = ASScrollNode()
self.selectedLineNode = ASImageNode() self.selectedLineNode = ASImageNode()
@ -153,7 +208,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
}) })
self.paneNodes[specifier.key] = paneNode 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] = [] var removeKeys: [PeerInfoPaneKey] = []
for (key, _) in self.paneNodes { for (key, _) in self.paneNodes {
@ -598,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear)
self.coveringBackgroundNode.isUserInteractionEnabled = false self.coveringBackgroundNode.isUserInteractionEnabled = false
self.tabsContainerNode = PeerInfoPaneTabsContainerNode() self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context)
self.tabsSeparatorNode = ASDisplayNode() self.tabsSeparatorNode = ASDisplayNode()
self.tabsSeparatorNode.isLayerBacked = true 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 self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String let title: String
var icons: [TelegramMediaFile] = []
switch key { switch key {
case .stories: case .stories:
title = presentationData.strings.PeerInfo_PaneStories title = presentationData.strings.PeerInfo_PaneStories
@ -1153,8 +1209,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
case .gifts: case .gifts:
title = presentationData.strings.PeerInfo_PaneGifts 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) }, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)
for (_, pane) in self.pendingPanes { for (_, pane) in self.pendingPanes {

View File

@ -38,7 +38,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var unlockBackground: ASDisplayNode? private var unlockBackground: NavigationBackgroundNode?
private var unlockSeparator: ASDisplayNode? private var unlockSeparator: ASDisplayNode?
private var unlockText: ComponentView<Empty>? private var unlockText: ComponentView<Empty>?
private var unlockButton: SolidRoundedButtonNode? private var unlockButton: SolidRoundedButtonNode?
@ -263,7 +263,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.theme = presentationData.theme self.theme = presentationData.theme
let unlockText: ComponentView<Empty> let unlockText: ComponentView<Empty>
let unlockBackground: ASDisplayNode let unlockBackground: NavigationBackgroundNode
let unlockSeparator: ASDisplayNode let unlockSeparator: ASDisplayNode
let unlockButton: SolidRoundedButtonNode let unlockButton: SolidRoundedButtonNode
if let current = self.unlockText { if let current = self.unlockText {
@ -276,7 +276,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if let current = self.unlockBackground { if let current = self.unlockBackground {
unlockBackground = current unlockBackground = current
} else { } else {
unlockBackground = ASDisplayNode() unlockBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor)
self.addSubnode(unlockBackground) self.addSubnode(unlockBackground)
self.unlockBackground = unlockBackground self.unlockBackground = unlockBackground
} }
@ -304,7 +304,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
if themeUpdated { 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 unlockSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme))
} }
@ -326,6 +326,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 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)) 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)) 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( let unlockSize = unlockText.update(

View File

@ -192,6 +192,7 @@ public final class ArchiveInfoContentComponent: Component {
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, lineSpacing: 0.2,
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL") return NSAttributedString.Key(rawValue: "URL")

View File

@ -189,6 +189,7 @@ public final class BirthdayPickerContentComponent: Component {
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, lineSpacing: 0.2,
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL") return NSAttributedString.Key(rawValue: "URL")

View File

@ -616,6 +616,7 @@ final class ChatbotSetupScreenComponent: Component {
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.25, lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL") return NSAttributedString.Key(rawValue: "URL")

View File

@ -490,6 +490,8 @@ private final class ParagraphComponent: CombinedComponent {
horizontalAlignment: .natural, horizontalAlignment: .natural,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -262,7 +262,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -933,7 +933,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 5, maximumNumberOfLines: 5,
lineSpacing: 0.2, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -458,7 +458,8 @@ final class StarsStatisticsScreenComponent: Component {
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .plain(balanceInfoString), text: .plain(balanceInfoString),
maximumNumberOfLines: 0, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -222,7 +222,8 @@ private final class SheetContent: CombinedComponent {
amountFooter = AnyComponent(MultilineTextComponent( amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString), text: .plain(amountInfoString),
maximumNumberOfLines: 0, 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 highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -232,7 +233,7 @@ private final class SheetContent: CombinedComponent {
}, },
tapAction: { attributes, _ in tapAction: { attributes, _ in
if let controller = controller() as? StarsWithdrawScreen, let navigationController = controller.navigationController as? NavigationController { 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: {})
} }
} }
)) ))

View File

@ -1390,7 +1390,7 @@ extension ChatControllerImpl {
if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in |> 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 return
} }
var originalMediaReference: AnyMediaReference? 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 { } else {
strongSelf.presentAttachmentMenu(subject: .default) strongSelf.presentAttachmentMenu(subject: .default)

View File

@ -8458,7 +8458,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities) let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities)
var state = state 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)) state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
} }
if !text.isEmpty { if !text.isEmpty {

View File

@ -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 _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings
@ -814,188 +814,184 @@ extension ChatControllerImpl {
hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat 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: { let controller = legacyAttachmentMenu(
self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in 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 { if !inputText.string.isEmpty {
strongSelf.clearInputText() 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!) strongSelf.editMessageMediaWithLegacySignals(signals!)
completion() completion()
} else { }, selectRecentlyUsedInlineBot: { [weak self] peer in
let immediateCompletion = getAnimatedTransitionSource == nil if let strongSelf = self, let addressName = peer.addressName {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
if !immediateCompletion { $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in
completion() return .text
} })
})
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
}) })
}) }
}, 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 controller.didDismiss = { [weak legacyController] _ in
legacyController?.dismiss() legacyController?.dismiss()
} }

View File

@ -1530,7 +1530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
isEditingMedia = !value.isEmpty isEditingMedia = !value.isEmpty
isMediaEnabled = !value.isEmpty isMediaEnabled = !value.isEmpty
} else { } else {
isMediaEnabled = false isMediaEnabled = true
} }
} }

View File

@ -1925,7 +1925,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} }
public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController { 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 { public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController {

View File

@ -209,11 +209,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
} }
} }
if !skipEntity { 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) string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks { if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) 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: case .BotCommand:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)

View File

@ -788,6 +788,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.dismiss() controller.dismiss()
case "web_app_open_tg_link": case "web_app_open_tg_link":
if let json = json, let path = json["path_full"] as? String { 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 controller.openUrl("https://t.me\(path)", false, { [weak controller] in
let _ = controller let _ = controller
// controller?.dismiss() // controller?.dismiss()