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