mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Various improvements
This commit is contained in:
parent
059af7d697
commit
89e3ae02a2
@ -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";
|
||||
|
@ -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, _):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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()))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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! {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -149,6 +149,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/UrlHandling",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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: {})
|
||||
}
|
||||
}
|
||||
))
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -1530,7 +1530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
isEditingMedia = !value.isEmpty
|
||||
isMediaEnabled = !value.isEmpty
|
||||
} else {
|
||||
isMediaEnabled = false
|
||||
isMediaEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user