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.ReceivedTitle" = "Received Gift";
"Gift.View.UnavailableTitle" = "Unavailable";
"Gift.View.KeepOrConvertDescription" = "You can keep this gift in your Profile or convert it to %@. [More About Stars >]()";
"Gift.View.KeepOrConvertDescription.Stars_1" = "%@ Star";
"Gift.View.KeepOrConvertDescription.Stars_any" = "%@ Stars";
@ -12972,20 +12973,36 @@ Sorry for the inconvenience.";
"Gift.View.OtherDescription" = "%1$@ can keep this gift in their Profile or convert it to %2$@. [More About Stars >]()";
"Gift.View.OtherDescription.Stars_1" = "%@ Star";
"Gift.View.OtherDescription.Stars_any" = "%@ Stars";
"Gift.View.UnavailableDescription" = "This gift has sold out";
"Gift.View.From" = "From";
"Gift.View.HiddenName" = "Hidden Name";
"Gift.View.Send" = "send a gift";
"Gift.View.Date" = "Date";
"Gift.View.FirstSale" = "First Sale";
"Gift.View.LastSale" = "Last Sale";
"Gift.View.Value" = "Value";
"Gift.View.Sale" = "sale for %@";
"Gift.View.Sale.Stars_1" = "%@ Star";
"Gift.View.Sale.Stars_any" = "%@ Stars";
"Gift.View.Availability" = "Availability";
"Gift.View.Availability.Of" = "%1$@ of %2$@";
"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left";
"Gift.View.Hide" = "Hide from My Page";
"Gift.View.Display" = "Display on My Page";
"Gift.View.Convert" = "Convert to %@";
"Gift.View.Convert.Stars_1" = "%@ Star";
"Gift.View.Convert.Stars_any" = "%@ Stars";
"Gift.View.DisplayedInfo" = "The gift is visible on your Page. [View >]()";
"Gift.View.HiddenInfo" = "This gift is hidden. Only you can see it.";
"Gift.Displayed.Title" = "Gift Saved to Profile";
"Gift.Displayed.Text" = "The gift is now displayed in [your profile]().";
"Gift.Displayed.NewText" = "The gift is now shown on your Page.";
"Gift.Displayed.View" = "View";
"Gift.Hidden.Title" = "Gift Removed from Profile";
"Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]().";
"Gift.Hidden.NewText" = "The gift is removed from your Page.";
"Gift.Convert.Title" = "Convert Gift to Stars";
"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift.";
"Gift.Convert.Stars_1" = "%@ Star";
@ -13077,3 +13094,6 @@ Sorry for the inconvenience.";
"WebBrowser.AuthChallenge.Title" = "Sign in to %@";
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";
"ChatList.Search.FilterPublicPosts" = "Public Posts";
"DialogList.SearchSectionPublicPosts" = "Public Posts";

View File

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

View File

@ -73,8 +73,13 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
url = updatedPath
}
let request = URLRequest(url: URL(fileURLWithPath: updatedPath))
self.webView.load(request)
let updatedUrl = URL(fileURLWithPath: updatedPath)
let request = URLRequest(url: updatedUrl)
if updatedPath.lowercased().hasSuffix(".txt"), let data = try? Data(contentsOf: updatedUrl) {
self.webView.load(data, mimeType: "text/plain", characterEncodingName: "UTF-8", baseURL: URL(string: "http://localhost")!)
} else {
self.webView.load(request)
}
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document)

View File

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

View File

@ -60,8 +60,9 @@ final class ChatListSearchInteraction {
let dismissInput: () -> Void
let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
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.openDisabledPeer = openDisabledPeer
self.openMessage = openMessage
@ -76,6 +77,7 @@ final class ChatListSearchInteraction {
self.dismissInput = dismissInput
self.getSelectedMessageIds = getSelectedMessageIds
self.openStories = openStories
self.switchToFilter = switchToFilter
}
}
@ -120,6 +122,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var suggestedFilters: [ChatListSearchFilter]?
private let suggestedFiltersDisposable = MetaDisposable()
private var forumPeer: EnginePeer?
private var hasPublicPostsTab = false
private var showPublicPostsTab = false
private var shareStatusDisposable: MetaDisposable?
@ -281,53 +285,27 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
avatarNode: sourceNode as? AvatarNode,
sharedProgressDisposable: self.sharedOpenStoryDisposable
)
}, switchToFilter: { [weak self] filter in
guard let self else {
return
}
if filter == .publicPosts && !self.showPublicPostsTab {
self.showPublicPostsTab = true
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
Queue.mainQueue().justDispatch {
self.paneContainerNode.requestSelectPane(filter)
}
})
self.paneContainerNode.interaction = interaction
self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in
if let strongSelf = self, let key = key {
var filterKey: ChatListSearchFilter
switch key {
case .chats:
filterKey = .chats
case .topics:
filterKey = .topics
case .channels:
filterKey = .channels
case .apps:
filterKey = .apps
case .media:
filterKey = .media
case .downloads:
filterKey = .downloads
case .links:
filterKey = .links
case .files:
filterKey = .files
case .music:
filterKey = .music
case .voice:
filterKey = .voice
}
strongSelf.selectedFilter = .filter(filterKey)
strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter))
strongSelf.transitionFraction = transitionFraction
if let (layout, _) = strongSelf.validLayout {
let filters: [ChatListSearchFilter]
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters
} else {
var isForum = false
if case .forum = strongSelf.location {
isForum = true
}
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && strongSelf.hasDownloads).map(\.filter)
}
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
}
guard let self, let key else {
return
}
self.currentPaneUpdated(key, transitionFraction: transitionFraction, transition: transition)
}
self.paneContainerNode.requesDismissInput = {
@ -368,6 +346,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music
case .voice:
key = .voice
case .publicPosts:
key = .publicPosts
case let .date(minDate, maxDate, title):
date = (minDate, maxDate, title)
case let .peer(id, isGroup, _, compactDisplayTitle):
@ -435,7 +415,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))))
}
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in
var suggestedFilters: [ChatListSearchFilter] = []
if !dates.isEmpty {
let formatter = DateFormatter()
@ -481,26 +461,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
existingPeerIds.insert(peer.id)
}
}
return suggestedFilters
return (suggestedFilters, searchQuery?.hasPrefix("#") ?? false)
}
|> deliverOnMainQueue).startStrict(next: { [weak self] filters in
|> deliverOnMainQueue).startStrict(next: { [weak self] filters, hasPublicPosts in
guard let strongSelf = self else {
return
}
var filteredFilters: [ChatListSearchFilter] = []
for filter in filters {
if case .date = filter, strongSelf.searchOptionsValue?.date == nil {
filteredFilters.append(filter)
}
if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil {
filteredFilters.append(filter)
if !hasPublicPosts {
for filter in filters {
if case .date = filter, strongSelf.searchOptionsValue?.date == nil {
filteredFilters.append(filter)
}
if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil {
filteredFilters.append(filter)
}
}
}
let previousFilters = strongSelf.suggestedFilters
strongSelf.suggestedFilters = filteredFilters
if filteredFilters != previousFilters {
let previousHasPublicPosts = strongSelf.hasPublicPostsTab
strongSelf.hasPublicPostsTab = hasPublicPosts
if !hasPublicPosts {
strongSelf.showPublicPostsTab = false
}
if filteredFilters != previousFilters || hasPublicPosts != previousHasPublicPosts {
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
@ -652,6 +640,52 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)))
}
private func currentPaneUpdated(_ key: ChatListSearchPaneKey, transitionFraction: CGFloat = 0.0, transition: ContainedViewLayoutTransition) {
var filterKey: ChatListSearchFilter
switch key {
case .chats:
filterKey = .chats
case .topics:
filterKey = .topics
case .channels:
filterKey = .channels
case .apps:
filterKey = .apps
case .media:
filterKey = .media
case .downloads:
filterKey = .downloads
case .links:
filterKey = .links
case .files:
filterKey = .files
case .music:
filterKey = .music
case .voice:
filterKey = .voice
case .publicPosts:
filterKey = .publicPosts
}
self.selectedFilter = .filter(filterKey)
self.selectedFilterPromise.set(.single(self.selectedFilter))
self.transitionFraction = transitionFraction
if let (layout, _) = self.validLayout {
let filters: [ChatListSearchFilter]
if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters
} else {
var isForum = false
if case .forum = self.location {
isForum = true
}
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter)
}
self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition)
}
}
public func search(filter: ChatListSearchFilter, query: String?) {
let key: ChatListSearchPaneKey
switch filter {
@ -713,7 +747,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty {
filters = suggestedFilters
} else {
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads).map(\.filter)
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter)
}
let overflowInset: CGFloat = 20.0
@ -891,7 +925,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let availablePanes: [ChatListSearchPaneKey]
if self.displaySearchFilters {
availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads)
availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.hasPublicPostsTab)
} else {
availablePanes = isForum ? [.topics] : [.chats]
}

View File

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

View File

@ -394,6 +394,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case generic
case downloading
case recentlyDownloaded
case publicPosts
}
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
@ -565,7 +566,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
}
}
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem {
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ListViewItem {
switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType):
let actionTitle: String?
@ -876,7 +877,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats, requiresPremiumForMessaging):
case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, section, allPaused, storyStats, requiresPremiumForMessaging):
let header: ChatListSearchItemHeader
switch orderingKey {
case .downloading:
@ -894,11 +895,22 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openClearRecentlyDownloaded()
})
case .index:
var headerType: ChatListSearchItemHeaderType = .messages(location: nil)
if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId {
headerType = .messages(location: peer.compactDisplayTitle)
if case .publicPosts = section {
if case .publicPosts = key {
header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} else {
//TODO:localize
header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Show More >", action: {
openPublicPosts()
})
}
} else {
var headerType: ChatListSearchItemHeaderType = .messages(location: nil)
if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId {
headerType = .messages(location: peer.compactDisplayTitle)
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
var isMedia = false
@ -1034,12 +1046,12 @@ private func chatListSearchContainerPreparedRecentTransition(
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty)
}
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerTransition {
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
}
@ -1373,6 +1385,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
tagMask = nil
case .topics:
tagMask = nil
case .publicPosts:
tagMask = nil
case .channels:
tagMask = nil
case .apps:
@ -1748,114 +1762,118 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
foundLocalPeers = .single(([], [:], Set()))
}
} else if let query = query, (key == .chats || key == .topics) {
let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers()
|> map { peers -> [RecentlySearchedPeer] in
let allIds = peers.map(\.peer.peerId)
let updatedState = previousRecentlySearchedPeersState.modify { current in
if var current = current, current.query == query {
current.ids = current.ids.filter { id in
allIds.contains(id)
}
return current
} else {
var state = SearchedPeersState()
state.ids = allIds
state.query = query
return state
}
}
var result: [RecentlySearchedPeer] = []
if let updatedState = updatedState {
for id in updatedState.ids {
for peer in peers {
if id == peer.peer.peerId {
result.append(peer)
if query.hasPrefix("#") {
foundLocalPeers = .single(([], [:], Set()))
} else {
let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers()
|> map { peers -> [RecentlySearchedPeer] in
let allIds = peers.map(\.peer.peerId)
let updatedState = previousRecentlySearchedPeersState.modify { current in
if var current = current, current.query == query {
current.ids = current.ids.filter { id in
allIds.contains(id)
}
return current
} else {
var state = SearchedPeersState()
state.ids = allIds
state.query = query
return state
}
}
}
return result
}
foundLocalPeers = combineLatest(
context.engine.contacts.searchLocalPeers(query: query.lowercased()),
fixedOrRemovedRecentlySearchedPeers
)
|> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional<EnginePeer.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
}
var result: [RecentlySearchedPeer] = []
if let updatedState = updatedState {
for id in updatedState.ids {
for peer in peers {
if id == peer.peer.peerId {
result.append(peer)
}
}
}
}
let unreadCount = unreadCounts[peer.peerId]
if let unreadCount = unreadCount, unreadCount > 0 {
unread[peer.peerId] = (Int32(unreadCount), isMuted)
return result
}
foundLocalPeers = combineLatest(
context.engine.contacts.searchLocalPeers(query: query.lowercased()),
fixedOrRemovedRecentlySearchedPeers
)
|> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional<EnginePeer.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 {
foundLocalPeers = combineLatest(
@ -2068,13 +2086,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case .savedMessagesChats = location {
foundRemotePeers = .single(([], [], false))
} else if let query = query, case .chats = key {
foundRemotePeers = (
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true))
|> then(
globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query)
|> map { ($0.0, $0.1, false) }
if query.hasPrefix("#") {
foundRemotePeers = .single(([], [], false))
} else {
foundRemotePeers = (
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true))
|> then(
globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query)
|> map { ($0.0, $0.1, false) }
)
)
)
}
} else if let query = query, case .channels = key {
foundRemotePeers = (
.single((currentRemotePeersValue.0, currentRemotePeersValue.1, true))
@ -2133,8 +2155,33 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError>
if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") {
let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 50)
foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true))
|> then(
searchSignal
|> map { result -> ([FoundRemoteMessages], Bool) in
let foundMessages = result.0
let messages: [EngineMessage]
if key == .chats {
messages = foundMessages.messages.prefix(3).map { EngineMessage($0) }
} else {
messages = foundMessages.messages.map { EngineMessage($0) }
}
return ([FoundRemoteMessages(messages: messages, readCounters: foundMessages.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)], false)
}
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
)
} else {
foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
}
let foundRemoteMessages: Signal<([FoundRemoteMessages], Bool), NoError>
if case .savedMessagesChats = location {
if key == .publicPosts {
foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
} else if case .savedMessagesChats = location {
foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
} else if peersFilter.contains(.doNotSearchMessages) {
foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
@ -2146,13 +2193,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in
let limit: Int32
#if DEBUG
limit = 50
#else
limit = 50
#endif
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: limit)
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
}
let searchSignal = combineLatest(searchSignals)
@ -2294,8 +2335,20 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
foundThreads = .single([])
}
return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads)
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in
return combineLatest(
accountPeer,
foundLocalPeers,
foundRemotePeers,
foundRemoteMessages,
foundPublicMessages,
presentationDataPromise.get(),
searchStatePromise.get(),
selectionPromise.get(),
resolvedMessage,
fixedRecentlySearchedPeers,
foundThreads
)
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in
let isSearching = foundRemotePeers.2 || foundRemoteMessages.1
var entries: [ChatListSearchEntry] = []
var index = 0
@ -2629,6 +2682,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var firstHeaderId: Int64?
if !foundRemotePeers.2 {
index = 0
var existingPostIds = Set<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>()
for foundRemoteMessageSet in foundRemoteMessages.0 {
for message in foundRemoteMessageSet.messages {
@ -3131,6 +3202,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
})
}, openStories: { peerId, avatarNode in
strongSelf.interaction.openStories?(peerId, avatarNode)
}, openPublicPosts: {
strongSelf.interaction.switchToFilter(.publicPosts)
})
strongSelf.currentEntries = newEntries
if strongSelf.key == .downloads {
@ -4663,7 +4736,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
switch key {
case .chats, .topics, .channels, .apps, .downloads:
case .chats, .topics, .channels, .apps, .downloads, .publicPosts:
let message = EngineMessage(
stableId: 0,
stableVersion: 0,

View File

@ -50,6 +50,7 @@ final class ChatListSearchPaneWrapper {
public enum ChatListSearchPaneKey {
case chats
case topics
case publicPosts
case channels
case apps
case media
@ -67,6 +68,8 @@ extension ChatListSearchPaneKey {
return .chats
case .topics:
return .topics
case .publicPosts:
return .publicPosts
case .channels:
return .channels
case .apps:
@ -87,13 +90,16 @@ extension ChatListSearchPaneKey {
}
}
func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatListSearchPaneKey] {
func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPosts: Bool) -> [ChatListSearchPaneKey] {
var result: [ChatListSearchPaneKey] = []
if isForum {
result.append(.topics)
} else {
result.append(.chats)
}
if hasPublicPosts {
result.append(.publicPosts)
}
result.append(.channels)
result.append(.apps)
result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice])

View File

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

View File

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

View File

@ -277,6 +277,7 @@ open class ImmediateTextView: TextView {
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var linkHighlightInset: UIEdgeInsets = .zero
public var trailingLineWidth: CGFloat?
@ -356,7 +357,7 @@ open class ImmediateTextView: TextView {
}
}
if let rects = rects {
if var rects, !rects.isEmpty {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
@ -366,7 +367,8 @@ open class ImmediateTextView: TextView {
strongSelf.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.bounds
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset)
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in

View File

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

View File

@ -63,7 +63,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.containerNode = ASDisplayNode()
self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in
self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil && controller.mode != .chatOnly, cancel: { [weak controller] in
controller?.dismiss()
})
@ -75,7 +75,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.recentListNode.alpha = 0.0
let navigationController = controller.navigationController as? NavigationController
if let peer, !controller.all {
if let peer, controller.mode != .noChat {
self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil)
self.currentController?.alwaysShowSearchResultsAsList = true
self.currentController?.showListEmptyResults = true
@ -117,7 +117,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.containerNode)
if controller.all {
if controller.mode == .noChat {
self.isSearching.set(self.myChatContents?.searching ?? .single(false))
} else {
if let _ = peer {

View File

@ -26,7 +26,11 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen()
if ([UIScreen mainScreen].scale >= 2.0f - FLT_EPSILON)
{
if (widescreenWidth >= 932.0f - FLT_EPSILON)
if (widescreenWidth >= 956.0f - FLT_EPSILON)
{
return CGSizeMake(145.0f + TGScreenPixel, 145.0 + TGScreenPixel);
}
else if (widescreenWidth >= 932.0f - FLT_EPSILON)
{
return CGSizeMake(141.0f + TGScreenPixel, 141.0 + TGScreenPixel);
}
@ -38,6 +42,10 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen()
{
return CGSizeMake(137.0f - TGScreenPixel, 137.0f - TGScreenPixel);
}
else if (widescreenWidth >= 874.0f - FLT_EPSILON)
{
return CGSizeMake(133.0f - TGScreenPixel, 133.0f - TGScreenPixel);
}
else if (widescreenWidth >= 852.0f - FLT_EPSILON)
{
return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel);

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)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -382,7 +413,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
}
itemViews.append(carouselItem)
let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
let galleryTitle: String
if addingMedia {
//TODO:localize
galleryTitle = "Add Photo or Video"
} else {
galleryTitle = editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo
}
let galleryItem = TGMenuSheetButtonItemView(title: galleryTitle, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openGallery()
})!
@ -395,11 +433,20 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
}
}
itemViews.append(galleryItem)
underlyingViews.append(galleryItem)
if addingMedia {
//TODO:localize
let fileItem = TGMenuSheetButtonItemView(title: "Add Document", type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
})!
itemViews.append(fileItem)
underlyingViews.append(fileItem)
}
}
if !editing {
if !editing && !addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
@ -408,7 +455,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
underlyingViews.append(fileItem)
}
if canEditFile {
if canEditFile && !addingMedia {
let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openFileGallery()
@ -488,7 +535,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit
itemViews.append(editCurrentItem)
}
if editMediaOptions == nil {
if editMediaOptions == nil && !addingMedia {
let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openMap()

View File

@ -651,7 +651,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId)
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: [])
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: fileSize(path), attributes: [.FileName(fileName: name)], alternativeRepresentations: [])
var attributes: [MessageAttribute] = []
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))

View File

@ -67,14 +67,7 @@ public final class ListSectionHeaderNode: ASDisplayNode {
}
}
if let action = self.action {
let actionColor: UIColor
switch self.actionType {
case .generic:
actionColor = self.theme.chatList.sectionHeaderTextColor
case .destructive:
actionColor = self.theme.list.itemDestructiveColor
}
self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: actionColor)
self.updateActionTitle()
self.actionButton?.accessibilityLabel = action
self.actionButton?.accessibilityTraits = [.button]
}
@ -115,16 +108,34 @@ public final class ListSectionHeaderNode: ASDisplayNode {
return super.hitTest(point, with: event)
}
private func updateActionTitle() {
guard let action = self.action else {
return
}
let actionColor: UIColor
switch self.actionType {
case .generic:
actionColor = self.theme.chatList.sectionHeaderTextColor
case .destructive:
actionColor = self.theme.list.itemDestructiveColor
}
let attributedText = NSMutableAttributedString(string: action, font: actionFont, textColor: actionColor)
if let range = attributedText.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") {
attributedText.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: attributedText.string))
attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string))
}
self.actionButtonLabel?.attributedText = attributedText
}
public func updateTheme(theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor
self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor)
self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor
if let action = self.action {
self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor)
}
self.updateActionTitle()
if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)

View File

@ -1034,7 +1034,8 @@ private final class SheetContent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { _ in
return nil
},

View File

@ -2572,7 +2572,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
footer: AnyComponent(MultilineTextComponent(
text: .plain(adsInfoString),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -254,6 +254,8 @@ private final class SheetContent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: linkColor.withMultipliedAlpha(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
case price
case convertStars
case availability
case soldOut
}
public struct Availability: Equatable, Codable, PostboxCoding {
@ -61,6 +62,31 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
}
}
public struct SoldOut: Equatable, Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case firstSale
case lastSale
}
public let firstSale: Int32
public let lastSale: Int32
public init(firstSale: Int32, lastSale: Int32) {
self.firstSale = firstSale
self.lastSale = lastSale
}
public init(decoder: PostboxDecoder) {
self.firstSale = decoder.decodeInt32ForKey(CodingKeys.firstSale.rawValue, orElse: 0)
self.lastSale = decoder.decodeInt32ForKey(CodingKeys.lastSale.rawValue, orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.firstSale, forKey: CodingKeys.firstSale.rawValue)
encoder.encodeInt32(self.lastSale, forKey: CodingKeys.lastSale.rawValue)
}
}
public enum DecodingError: Error {
case generic
}
@ -70,13 +96,15 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
public let price: Int64
public let convertStars: Int64
public let availability: Availability?
public let soldOut: SoldOut?
public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?) {
public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?) {
self.id = id
self.file = file
self.price = price
self.convertStars = convertStars
self.availability = availability
self.soldOut = soldOut
}
public init(from decoder: Decoder) throws {
@ -92,6 +120,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
self.price = try container.decode(Int64.self, forKey: .price)
self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0
self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability)
self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut)
}
public init(decoder: PostboxDecoder) {
@ -100,6 +129,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0)
self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0)
self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { StarGift.Availability(decoder: $0) }) as? StarGift.Availability
self.soldOut = decoder.decodeObjectForKey(CodingKeys.soldOut.rawValue, decoder: { StarGift.SoldOut(decoder: $0) }) as? StarGift.SoldOut
}
public func encode(to encoder: Encoder) throws {
@ -114,6 +144,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
try container.encode(self.price, forKey: .price)
try container.encode(self.convertStars, forKey: .convertStars)
try container.encodeIfPresent(self.availability, forKey: .availability)
try container.encodeIfPresent(self.soldOut, forKey: .soldOut)
}
public func encode(_ encoder: PostboxEncoder) {
@ -126,21 +157,30 @@ public struct StarGift: Equatable, Codable, PostboxCoding {
} else {
encoder.encodeNil(forKey: CodingKeys.availability.rawValue)
}
if let soldOut = self.soldOut {
encoder.encodeObject(soldOut, forKey: CodingKeys.soldOut.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.soldOut.rawValue)
}
}
}
extension StarGift {
init?(apiStarGift: Api.StarGift) {
switch apiStarGift {
case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars):
case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale):
var availability: Availability?
if let availabilityRemains, let availabilityTotal {
availability = Availability(remains: availabilityRemains, total: availabilityTotal)
}
var soldOut: SoldOut?
if let firstSale, let lastSale {
soldOut = SoldOut(firstSale: firstSale, lastSale: lastSale)
}
guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else {
return nil
}
self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability)
self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut)
}
}
}
@ -260,6 +300,7 @@ private final class ProfileGiftsContextImpl {
private var count: Int32?
private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil)
var _state: ProfileGiftsContext.State?
private let stateValue = Promise<ProfileGiftsContext.State>()
var state: Signal<ProfileGiftsContext.State, NoError> {
return self.stateValue.get()
@ -355,6 +396,7 @@ private final class ProfileGiftsContextImpl {
}
private func pushState() {
self._state = ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState)
self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState)))
}
}
@ -438,6 +480,14 @@ public final class ProfileGiftsContext {
impl.convertStarGift(messageId: messageId)
}
}
public var currentState: ProfileGiftsContext.State? {
var state: ProfileGiftsContext.State?
self.impl.syncWith { impl in
state = impl._state
}
return state
}
}
private extension ProfileGiftsContext.State.StarGift {

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

View File

@ -106,6 +106,9 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
selectedFile = telegramFile
}
}
if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile {
selectedFile = file
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
@ -135,7 +138,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
}
}
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile)
let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments(
context: item.context,

View File

@ -303,7 +303,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
@objc private func progressPressed() {
if let resourceStatus = self.resourceStatus {
if let _ = self.arguments?.attributes.updatingMedia {
if let message = self.message {
self.context?.account.pendingUpdateMessageManager.cancel(messageId: message.id)
}
}
else if let resourceStatus = self.resourceStatus {
switch resourceStatus.mediaStatus {
case let .fetchStatus(fetchStatus):
if let context = self.context, let message = self.message, message.flags.isSending {
@ -590,10 +595,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusUpdated = true
}
let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo
var hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo
var hasThumbnailImage = !arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil
if case let .update(media) = arguments.attributes.updatingMedia?.media, let file = media.media as? TelegramMediaFile {
hasThumbnail = largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil || file.mimeType.hasPrefix("image/")
hasThumbnailImage = hasThumbnail
}
if mediaUpdated {
if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil {
if hasThumbnailImage {
updateImageSignal = chatMessageImageFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true)
}
@ -1622,59 +1632,64 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
switch resourceStatus.mediaStatus {
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
fetchStatus = resourceStatus.fetchStatus
}
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
var wasCheck = false
if let statusNode = self.statusNode, case .check = statusNode.state {
wasCheck = true
if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media {
let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027)
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
} else {
switch resourceStatus.mediaStatus {
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
fetchStatus = resourceStatus.fetchStatus
}
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
if isAudio && !isVoice && !isSending {
state = .play
} else {
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
state = .check(appearance: nil)
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
var wasCheck = false
if let statusNode = self.statusNode, case .check = statusNode.state {
wasCheck = true
}
if isAudio && !isVoice && !isSending {
state = .play
} else {
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
state = .check(appearance: nil)
} else {
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
}
}
case .Local:
if isAudio {
state = .play
} else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage)
} else {
state = .none
}
case .Remote, .Paused:
if isAudio && !isVoice {
state = .play
} else {
state = .download
}
}
case .Local:
if isAudio {
state = .play
} else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage)
case let .playbackStatus(playbackStatus):
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage
if isViewOnceMessage && playbackStatus == .playing {
state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel))
if incoming {
self.consumableContentNode.isHidden = true
}
} else {
state = .none
}
case .Remote, .Paused:
if isAudio && !isVoice {
state = .play
} else {
state = .download
}
}
case let .playbackStatus(playbackStatus):
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage
if isViewOnceMessage && playbackStatus == .playing {
state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel))
if incoming {
self.consumableContentNode.isHidden = true
}
} else {
switch playbackStatus {
case .playing:
state = .pause
case .paused:
state = .play
switch playbackStatus {
case .playing:
state = .pause
case .paused:
state = .play
}
}
}
}

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import GiftItemComponent
import InAppPurchaseManager
import TabSelectorComponent
import GiftSetupScreen
import GiftViewScreen
import UndoUI
final class GiftOptionsScreenComponent: Component {
@ -289,7 +290,19 @@ final class GiftOptionsScreenComponent: Component {
self.starsItems[itemId] = visibleItem
}
let isSoldOut = gift.availability?.remains == 0
var ribbon: GiftItemComponent.Ribbon?
if let _ = gift.soldOut {
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_SoldOut,
color: .red
)
} else if let _ = gift.availability {
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Limited,
color: .blue
)
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -300,14 +313,9 @@ final class GiftOptionsScreenComponent: Component {
theme: environment.theme,
peer: nil,
subject: .starGift(gift.id, gift.file),
price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)",
ribbon: gift.availability != nil ?
GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Limited,
color: .blue
)
: nil,
isSoldOut: isSoldOut
price: "⭐️ \(gift.price)",
ribbon: ribbon,
isSoldOut: gift.soldOut != nil
)
),
effectAlignment: .center,
@ -321,16 +329,11 @@ final class GiftOptionsScreenComponent: Component {
mainController = controller
}
if gift.availability?.remains == 0 {
self.dismissAllTooltips(controller: mainController)
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil),
elevatedLayout: false,
action: { _ in return true }
let giftController = GiftViewScreen(
context: component.context,
subject: .soldOutGift(gift)
)
mainController.present(resultController, in: .window(.root))
HapticFeedback().error()
mainController.push(giftController)
} else {
let giftController = GiftSetupScreen(
context: component.context,
@ -340,6 +343,7 @@ final class GiftOptionsScreenComponent: Component {
)
mainController.push(giftController)
}
}
}
},
@ -601,7 +605,8 @@ final class GiftOptionsScreenComponent: Component {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.2),
highlightColor: accentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -789,7 +794,8 @@ final class GiftOptionsScreenComponent: Component {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.2),
highlightColor: accentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -35,6 +35,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let openMyGifts: () -> Void
init(
context: AccountContext,
@ -43,7 +45,9 @@ private final class GiftViewSheetContent: CombinedComponent {
openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
openMyGifts: @escaping () -> Void
) {
self.context = context
self.subject = subject
@ -52,6 +56,8 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.openMyGifts = openMyGifts
}
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
@ -74,6 +80,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
var inProgress = false
@ -134,12 +141,10 @@ private final class GiftViewSheetContent: CombinedComponent {
let closeButton = Child(Button.self)
let animation = Child(GiftAnimationComponent.self)
let title = Child(MultilineTextComponent.self)
let amount = Child(BalancedTextComponent.self)
let amountStar = Child(BundleIconComponent.self)
let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let additionalText = Child(MultilineTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
let secondaryButton = Child(SolidRoundedButtonComponent.self)
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
@ -154,8 +159,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let closeImage: UIImage
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image
@ -175,35 +179,41 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate
)
let titleString: String
let animationFile: TelegramMediaFile?
let stars: Int64
let convertStars: Int64
let text: String?
let entities: [MessageTextEntity]?
let limitTotal: Int32?
var outgoing = false
var incoming = false
var savedToProfile = false
var converted = false
var giftId: Int64 = 0
var date: Int32 = 0
if let arguments = component.subject.arguments {
var date: Int32?
var soldOut = false
if case let .soldOutGift(gift) = component.subject {
animationFile = gift.file
stars = gift.price
text = nil
entities = nil
limitTotal = gift.availability?.total
convertStars = 0
soldOut = true
titleString = strings.Gift_View_UnavailableTitle
} else if let arguments = component.subject.arguments {
animationFile = arguments.gift.file
stars = arguments.gift.price
text = arguments.text
entities = arguments.entities
limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile
converted = arguments.converted
giftId = arguments.gift.id
date = arguments.date
titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title
} else {
animationFile = nil
stars = 0
@ -211,10 +221,13 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = nil
limitTotal = nil
convertStars = 0
titleString = ""
}
var descriptionText: String
if incoming {
if soldOut {
descriptionText = strings.Gift_View_UnavailableDescription
} else if incoming {
if !converted {
descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string
} else {
@ -242,19 +255,11 @@ private final class GiftViewSheetContent: CombinedComponent {
}
descriptionText = modifiedString
}
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
if outgoing {
formattedAmount = "- \(formattedAmount)"
}
let countFont: UIFont = Font.semibold(17.0)
let amountText = formattedAmount
let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title,
string: titleString,
font: Font.bold(25.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
@ -266,27 +271,6 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate
)
let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor)
let amount = amount.update(
component: BalancedTextComponent(
text: .plain(amountAttributedText),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let amountStar = amountStar.update(
component: BundleIconComponent(
name: "Premium/Stars/StarMedium",
tintColor: nil
),
availableSize: context.availableSize,
transition: .immediate
)
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
@ -296,13 +280,52 @@ private final class GiftViewSheetContent: CombinedComponent {
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
tableItems.append(.init(
id: "from",
title: strings.Gift_View_From,
component: AnyComponent(
Button(
if !soldOut {
if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
let fromComponent: AnyComponent<Empty>
if incoming {
fromComponent = AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: peer
)
),
action: {
component.openPeer(peer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: strings.Gift_View_Send,
color: theme.list.itemAccentColor
)),
action: {
component.sendGift(peerId)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
))
)
], spacing: 4.0)
)
} else {
fromComponent = AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
@ -312,37 +335,114 @@ private final class GiftViewSheetContent: CombinedComponent {
)
),
action: {
if "".isEmpty {
component.openPeer(peer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
component.openPeer(peer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
))
}
tableItems.append(.init(
id: "from",
title: strings.Gift_View_From,
component: fromComponent
))
} else {
tableItems.append(.init(
id: "from_anon",
title: strings.Gift_View_From,
component: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil
)
)
))
}
}
if case let .soldOutGift(gift) = component.subject, let soldOut = gift.soldOut {
tableItems.append(.init(
id: "firstDate",
title: strings.Gift_View_FirstSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
} else {
tableItems.append(.init(
id: "from_anon",
title: strings.Gift_View_From,
id: "lastDate",
title: strings.Gift_View_LastSale,
component: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil
)
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
} else if let date {
tableItems.append(.init(
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
}
tableItems.append(.init(
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
let valueComponent: AnyComponent<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 {
@ -356,7 +456,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "availability",
title: strings.Gift_View_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
))
}
@ -390,7 +490,6 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate
)
let textFont = Font.regular(15.0)
let linkColor = theme.actionSheet.controlAccentColor
context.add(title
@ -413,15 +512,19 @@ private final class GiftViewSheetContent: CombinedComponent {
)
originY += animation.size.height
}
originY += 69.0
originY += 80.0
if soldOut {
originY -= 12.0
}
var descriptionSize: CGSize = .zero
if !descriptionText.isEmpty {
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textColor = theme.list.itemPrimaryTextColor
let textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0)
let textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
@ -435,7 +538,8 @@ private final class GiftViewSheetContent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -450,58 +554,70 @@ private final class GiftViewSheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
descriptionSize = description.size
var descriptionOrigin = originY
if "".isEmpty {
descriptionOrigin += amount.size.height + 13.0
}
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0))
)
originY += description.size.height + 10.0
} else {
originY += 11.0
}
let amountSpacing: CGFloat = 1.0
let totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width
let amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0
var amountOrigin = originY
if "".isEmpty {
amountOrigin -= descriptionSize.height + 10.0
if descriptionSize.height > 0 {
originY += amount.size.height + 26.0
} else {
originY += amount.size.height + 2.0
originY += description.size.height + 21.0
if soldOut {
originY -= 7.0
}
} else {
originY += amount.size.height + 20.0
originY += 21.0
}
let amountLabelOriginX: CGFloat
let amountStarOriginX: CGFloat
if !"".isEmpty {
amountStarOriginX = amountOriginX + amountStar.size.width / 2.0
amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0
} else {
amountLabelOriginX = amountOriginX + amount.size.width / 2.0
amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0
}
context.add(amount
.position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0))
)
context.add(amountStar
.position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel))
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
)
originY += table.size.height + 23.0
if incoming && !converted {
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
let descriptionText = savedToProfile ? strings.Gift_View_DisplayedInfo : strings.Gift_View_HiddenInfo
let textFont = Font.regular(13.0)
let textColor = theme.list.itemSecondaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
originY -= 5.0
let additionalText = additionalText.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.openMyGifts()
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(additionalText
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0))
)
originY += additionalText.size.height
originY += 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display,
@ -528,32 +644,6 @@ private final class GiftViewSheetContent: CombinedComponent {
)
originY += button.size.height
originY += 7.0
let secondaryButton = secondaryButton.update(
component: SolidRoundedButtonComponent(
title: strings.Gift_View_Convert(strings.Gift_View_Convert_Stars(Int32(convertStars))).string,
theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor),
font: .regular,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: false,
iconName: nil,
animationName: nil,
iconPosition: .left,
isLoading: false,
action: {
component.convertToStars()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
let secondaryButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: secondaryButton.size)
context.add(secondaryButton
.position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY))
)
originY += secondaryButton.size.height
} else {
let button = button.update(
component: SolidRoundedButtonComponent(
@ -603,6 +693,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
let sendGift: (EnginePeer.Id) -> Void
let openMyGifts: () -> Void
init(
context: AccountContext,
@ -610,7 +702,9 @@ private final class GiftViewSheetComponent: CombinedComponent {
openPeer: @escaping (EnginePeer) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void
openStarsIntro: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
openMyGifts: @escaping () -> Void
) {
self.context = context
self.subject = subject
@ -618,6 +712,8 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
self.sendGift = sendGift
self.openMyGifts = openMyGifts
}
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
@ -660,7 +756,9 @@ private final class GiftViewSheetComponent: CombinedComponent {
openPeer: context.component.openPeer,
updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro
openStarsIntro: context.component.openStarsIntro,
sendGift: context.component.sendGift,
openMyGifts: context.component.openMyGifts
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
@ -730,6 +828,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
case soldOutGift(StarGift)
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
switch self {
@ -739,6 +838,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
case let .profileGift(peerId, gift):
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
case .soldOutGift:
return nil
}
return nil
}
@ -762,6 +863,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)?
var sendGiftImpl: ((EnginePeer.Id) -> Void)?
var openMyGiftsImpl: (() -> Void)?
super.init(
context: context,
@ -779,6 +882,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
},
openStarsIntro: {
openStarsIntroImpl?()
},
sendGift: { peerId in
sendGiftImpl?(peerId)
},
openMyGifts: {
openMyGiftsImpl?()
}
),
navigationBarAppearance: .none,
@ -820,20 +929,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.dismissAnimated()
let title: String = added ? presentationData.strings.Gift_Displayed_Title : presentationData.strings.Gift_Hidden_Title
var text = added ? presentationData.strings.Gift_Displayed_Text : presentationData.strings.Gift_Hidden_Text
if let _ = updateSavedToProfile {
text = text.replacingOccurrences(of: "]()", with: "").replacingOccurrences(of: "[", with: "")
}
if let navigationController {
let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText
if let navigationController = self.navigationController as? NavigationController {
Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: title, text: text, undoText: nil, customAction: nil),
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { [weak navigationController] action in
if case .info = action, let navigationController {
if case .undo = action, let navigationController {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
@ -919,6 +1024,40 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
self.push(introController)
}
sendGiftImpl = { [weak self] peerId in
guard let self else {
return
}
let _ = (context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { giftOptions in
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions)
self.push(controller)
})
}
openMyGiftsImpl = { [weak self] in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .myProfileGifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController.pushViewController(controller, animated: true)
}
})
}
}
required public init(coder aDecoder: NSCoder) {
@ -1332,3 +1471,98 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
context.strokePath()
})
}
private final class ButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
public init(
context: AccountContext,
text: String,
color: UIColor
) {
self.context = context
self.text = text
self.color = color
}
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIView {
private var component: ButtonContentComponent?
private weak var componentState: EmptyComponentState?
private let backgroundLayer = SimpleLayer()
private let title = ComponentView<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(
text: .plain(paidReactionsFooterText),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")

View File

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

View File

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

View File

@ -13,6 +13,8 @@ import PeerInfoVisualMediaPaneNode
import PeerInfoPaneNode
import PeerInfoChatListPaneNode
import PeerInfoChatPaneNode
import TextFormat
import EmojiTextAttachmentView
final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey
@ -41,6 +43,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode
private var iconLayers: [InlineStickerItemLayer] = []
private var isSelected: Bool = false
@ -64,10 +67,46 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
self.pressed()
}
func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) {
func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) {
self.isSelected = isSelected
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
if !icons.isEmpty {
if self.iconLayers.isEmpty {
for icon in icons {
let iconSize = CGSize(width: 24.0, height: 24.0)
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: icon.fileId.id,
file: icon
)
let animationLayer = InlineStickerItemLayer(
context: .account(context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: icon,
cache: context.animationCache,
renderer: context.animationRenderer,
unique: true,
placeholderColor: presentationData.theme.list.mediaPlaceholderColor,
pointSize: iconSize,
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.iconLayers.append(animationLayer)
self.layer.addSublayer(animationLayer)
}
}
} else {
for layer in self.iconLayers {
layer.removeFromSuperlayer()
}
self.iconLayers.removeAll()
}
self.buttonNode.accessibilityLabel = title
self.buttonNode.accessibilityTraits = [.button]
if isSelected {
@ -76,9 +115,22 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
}
func updateLayout(height: CGFloat) -> CGFloat {
var totalWidth: CGFloat = 0.0
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
return titleSize.width
totalWidth = titleSize.width
if !self.iconLayers.isEmpty {
totalWidth += 1.0
let iconSize = CGSize(width: 24.0, height: 24.0)
let spacing: CGFloat = 1.0
for iconlayer in self.iconLayers {
iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 12.0), size: iconSize)
totalWidth += iconSize.width + spacing
}
totalWidth -= spacing
}
return totalWidth
}
func updateArea(size: CGSize, sideInset: CGFloat) {
@ -89,6 +141,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
struct PeerInfoPaneSpecifier: Equatable {
var key: PeerInfoPaneKey
var title: String
var icons: [TelegramMediaFile]
}
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
@ -96,6 +149,7 @@ private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGF
}
final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
private let context: AccountContext
private let scrollNode: ASScrollNode
private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:]
private let selectedLineNode: ASImageNode
@ -104,7 +158,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
var requestSelectPane: ((PeerInfoPaneKey) -> Void)?
override init() {
init(context: AccountContext) {
self.context = context
self.scrollNode = ASScrollNode()
self.selectedLineNode = ASImageNode()
@ -153,7 +208,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
})
self.paneNodes[specifier.key] = paneNode
}
paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData)
paneNode.updateText(context: self.context, title: specifier.title, icons: specifier.icons, isSelected: selectedPane == specifier.key, presentationData: presentationData)
}
var removeKeys: [PeerInfoPaneKey] = []
for (key, _) in self.paneNodes {
@ -598,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear)
self.coveringBackgroundNode.isUserInteractionEnabled = false
self.tabsContainerNode = PeerInfoPaneTabsContainerNode()
self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context)
self.tabsSeparatorNode = ASDisplayNode()
self.tabsSeparatorNode.isLayerBacked = true
@ -1122,6 +1177,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String
var icons: [TelegramMediaFile] = []
switch key {
case .stories:
title = presentationData.strings.PeerInfo_PaneStories
@ -1153,8 +1209,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
case .gifts:
title = presentationData.strings.PeerInfo_PaneGifts
icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).map { $0.gift.file } ?? []
}
return PeerInfoPaneSpecifier(key: key, title: title)
return PeerInfoPaneSpecifier(key: key, title: title, icons: icons)
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)
for (_, pane) in self.pendingPanes {

View File

@ -38,7 +38,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let backgroundNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var unlockBackground: ASDisplayNode?
private var unlockBackground: NavigationBackgroundNode?
private var unlockSeparator: ASDisplayNode?
private var unlockText: ComponentView<Empty>?
private var unlockButton: SolidRoundedButtonNode?
@ -263,7 +263,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.theme = presentationData.theme
let unlockText: ComponentView<Empty>
let unlockBackground: ASDisplayNode
let unlockBackground: NavigationBackgroundNode
let unlockSeparator: ASDisplayNode
let unlockButton: SolidRoundedButtonNode
if let current = self.unlockText {
@ -276,7 +276,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if let current = self.unlockBackground {
unlockBackground = current
} else {
unlockBackground = ASDisplayNode()
unlockBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor)
self.addSubnode(unlockBackground)
self.unlockBackground = unlockBackground
}
@ -304,7 +304,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
if themeUpdated {
unlockBackground.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
unlockBackground.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
unlockSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme))
}
@ -326,6 +326,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let bottomPanelHeight = bottomInset + buttonSize.height + 8.0
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight))
unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel))
let unlockSize = unlockText.update(

View File

@ -192,6 +192,7 @@ public final class ArchiveInfoContentComponent: Component {
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")

View File

@ -189,6 +189,7 @@ public final class BirthdayPickerContentComponent: Component {
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightInset: mainText.string.contains(">") ? UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0) : .zero,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")

View File

@ -616,6 +616,7 @@ final class ChatbotSetupScreenComponent: Component {
maximumNumberOfLines: 0,
lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")

View File

@ -490,6 +490,8 @@ private final class ParagraphComponent: CombinedComponent {
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -262,7 +262,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -933,7 +933,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -458,7 +458,8 @@ final class StarsStatisticsScreenComponent: Component {
footer: AnyComponent(MultilineTextComponent(
text: .plain(balanceInfoString),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)

View File

@ -222,7 +222,8 @@ private final class SheetContent: CombinedComponent {
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -232,7 +233,7 @@ private final class SheetContent: CombinedComponent {
},
tapAction: { attributes, _ in
if let controller = controller() as? StarsWithdrawScreen, let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
))

View File

@ -1390,7 +1390,7 @@ extension ChatControllerImpl {
if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { message in
guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else {
guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState else {
return
}
var originalMediaReference: AnyMediaReference?
@ -1405,7 +1405,11 @@ extension ChatControllerImpl {
}
}
}
strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference)
var editMediaOptions: MessageMediaEditingOptions?
if case let .media(options) = editMessageState.content {
editMediaOptions = options
}
strongSelf.presentEditingAttachmentMenu(editMediaOptions: editMediaOptions, editMediaReference: originalMediaReference)
})
} else {
strongSelf.presentAttachmentMenu(subject: .default)

View File

@ -8458,7 +8458,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities)
var state = state
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
if let editMessageState = state.editMessageState {
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
}
if !text.isEmpty {

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 entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings
@ -814,188 +814,184 @@ extension ChatControllerImpl {
hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat
}
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: hasSchedule, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: {
self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in
let controller = legacyAttachmentMenu(
context: strongSelf.context,
peer: strongSelf.presentationInterfaceState.renderedPeer?.peer,
threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation,
editMediaOptions: menuEditMediaOptions,
addingMedia: editMediaOptions == nil,
saveEditedPhotos: settings.storeEditedPhotos,
allowGrouping: true,
hasSchedule: hasSchedule,
canSendPolls: canSendPolls,
updatedPresentationData: strongSelf.updatedPresentationData,
parentController: legacyController,
recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue,
initialCaption: inputText,
openGallery: {
self?.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
self?.editMessageMediaWithLegacySignals(signals)
})
}, openCamera: { [weak self] cameraView, menuController in
if let strongSelf = self {
var enablePhoto = true
var enableVideo = true
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
enableVideo = false
}
var bannedSendPhotos: (Int32, Bool)?
var bannedSendVideos: (Int32, Bool)?
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos) {
bannedSendVideos = value
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
}
}
if bannedSendPhotos != nil {
enablePhoto = false
}
if bannedSendVideos != nil {
enableVideo = false
}
var storeCapturedPhotos = false
var hasSchedule = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat
hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat
}
presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
strongSelf.editMessageMediaWithLegacySignals(signals!)
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self {
if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) {
strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil)
}
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: false)
})
}
}, openFileGallery: {
self?.presentFileMediaPickerOptions(editingMessage: true)
}, openWebSearch: { [weak self] in
self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}, openMap: {
self?.presentLocationPicker()
}, openContacts: {
self?.presentContactPicker()
}, openPoll: {
if let controller = self?.configurePollCreation() {
self?.effectiveNavigationController?.pushViewController(controller)
}
}, presentSelectionLimitExceeded: {
guard let strongSelf = self else {
return
}
let text: String
if slowModeEnabled {
text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached
} else {
text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentCantSendMultipleFiles: {
guard let strongSelf = self else {
return
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentJpegConversionAlert: { completion in
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: {
completion(false)
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: {
completion(true)
})], actionLayout: .vertical), in: .window(.root))
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
guard let strongSelf = self else {
completion()
return
}
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
if editMediaOptions != nil {
self?.editMessageMediaWithLegacySignals(signals)
} else {
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
}
})
}, openCamera: { [weak self] cameraView, menuController in
if let strongSelf = self {
var enablePhoto = true
var enableVideo = true
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
enableVideo = false
}
var bannedSendPhotos: (Int32, Bool)?
var bannedSendVideos: (Int32, Bool)?
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos) {
bannedSendVideos = value
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
}
}
if bannedSendPhotos != nil {
enablePhoto = false
}
if bannedSendVideos != nil {
enableVideo = false
}
var storeCapturedPhotos = false
var hasSchedule = false
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat
hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat
}
presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
if editMediaOptions != nil {
strongSelf.editMessageMediaWithLegacySignals(signals!)
} else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
}
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self {
if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) {
strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil)
}
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: false)
})
}
}, openFileGallery: {
self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil)
}, openWebSearch: { [weak self] in
self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}, openMap: {
self?.presentLocationPicker()
}, openContacts: {
self?.presentContactPicker()
}, openPoll: {
if let controller = self?.configurePollCreation() {
self?.effectiveNavigationController?.pushViewController(controller)
}
}, presentSelectionLimitExceeded: {
guard let strongSelf = self else {
return
}
let text: String
if slowModeEnabled {
text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached
} else {
text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentCantSendMultipleFiles: {
guard let strongSelf = self else {
return
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentJpegConversionAlert: { completion in
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: {
completion(false)
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: {
completion(true)
})], actionLayout: .vertical), in: .window(.root))
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
guard let strongSelf = self else {
completion()
return
}
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
if editMediaOptions != nil {
strongSelf.editMessageMediaWithLegacySignals(signals!)
completion()
} else {
let immediateCompletion = getAnimatedTransitionSource == nil
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: {
if !immediateCompletion {
completion()
}
})
if immediateCompletion {
completion()
}
}
}, selectRecentlyUsedInlineBot: { [weak self] peer in
if let strongSelf = self, let addressName = peer.addressName {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in
return .text
}, selectRecentlyUsedInlineBot: { [weak self] peer in
if let strongSelf = self, let addressName = peer.addressName {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in
return .text
})
})
})
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: false)
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView(isFile: false)
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
)
controller.didDismiss = { [weak legacyController] _ in
legacyController?.dismiss()
}

View File

@ -1530,7 +1530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
isEditingMedia = !value.isEmpty
isMediaEnabled = !value.isEmpty
} 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 {
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 {

View File

@ -209,11 +209,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
}
}
if !skipEntity {
var hashtagValue = hashtag
var peerNameValue: String?
if hashtagValue.contains("@") {
let components = hashtagValue.components(separatedBy: "@")
if components.count == 2, let firstComponent = components.first, let lastComponent = components.last, !firstComponent.isEmpty && !lastComponent.isEmpty {
hashtagValue = firstComponent
peerNameValue = lastComponent
}
}
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range)
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerNameValue, hashtag: hashtagValue), range: range)
}
case .BotCommand:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)

View File

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