Various improvements

This commit is contained in:
Ilya Laktyushin 2025-03-23 03:56:44 +04:00
parent fa93715135
commit f720277d29
19 changed files with 851 additions and 631 deletions

View File

@ -14094,3 +14094,5 @@ Sorry for the inconvenience.";
"Gift.Unpin.Title" = "Too Manu Pinned Gifts";
"Gift.Unpin.Subtitle" = "Select a gift to unpin below:";
"Gift.Unpin.Unpin" = "Unpin";
"ChatList.Search.Ad" = "Ad";

View File

@ -6219,7 +6219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
title: title,
options: options,
completed: {
//removeAd?(adAttribute.opaqueId)
}
)
)
@ -6236,9 +6235,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let navigationController = self?.navigationController as? NavigationController else {
return
}
c?.dismiss(completion: {
if context.isPremium && !"".isEmpty {
//removeAd?(adAttribute.opaqueId)
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
if context.isPremium {
self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.ReportAd_Hidden, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in
return true
}), in: .current)
let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start()
if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
searchContentNode.removeAds()
}
} else {
var replaceImpl: ((ViewController) -> Void)?
let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: {

View File

@ -570,6 +570,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.selectionPanelNode?.selectedMessages = self.stateValue.selectedMessageIds ?? []
}
public func removeAds() {
for pane in self.paneContainerNode.currentPanes.values {
pane.node.removeAds()
}
}
private var currentSearchOptions: ChatListSearchOptions {
return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, date: nil)
}

View File

@ -858,19 +858,12 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
context.engine.messages.markAdAction(opaqueId: peer.opaqueId, media: false, fullscreen: false)
}, disabledAction: { _ in
interaction.disabledPeerSelected(peer.peer, nil, .generic)
}, contextAction: peerContextAction.flatMap { peerContextAction in
return { node, gesture, location in
peerContextAction(peer.peer, .search(nil), node, gesture, location)
}
}, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: nil, openStories: { itemPeer, sourceNode in
guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else {
return
}
if let sourceNode = sourceNode as? ContactsPeerItemNode {
openStories(peer.id, sourceNode.avatarNode)
}
}, adButtonAction: { node in
}, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: nil, adButtonAction: { node in
interaction.openAdInfo(node, peer)
}, visibilityUpdated: { isVisible in
if isVisible {
context.engine.messages.markAdAsSeen(opaqueId: peer.opaqueId)
}
})
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, isSelf):
let primaryPeer: EnginePeer
@ -1613,6 +1606,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private var deletedMessagesDisposable: Disposable?
private var adsHiddenPromise = ValuePromise<Bool>(false)
private var adsHidden = false {
didSet {
self.adsHiddenPromise.set(self.adsHidden)
}
}
private var searchQueryValue: String?
private var searchOptionsValue: ChatListSearchOptions?
@ -1954,6 +1954,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let previousRecentlySearchedPeersState = Atomic<SearchedPeersState?>(value: nil)
let hadAnySearchMessages = Atomic<Bool>(value: false)
let adsHiddenPromise = self.adsHiddenPromise
let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, self.searchScopePromise.get(), downloadItems)
|> mapToSignal { [weak self] query, options, searchScope, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) {
@ -2726,9 +2728,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
selectionPromise.get(),
resolvedMessage,
fixedRecentlySearchedPeers,
foundThreads
foundThreads,
adsHiddenPromise.get()
)
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in
|> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads, adsHidden -> ([ChatListSearchEntry], Bool)? in
let isSearching = foundRemotePeers.3 || foundRemoteMessages.1 || foundPublicMessages.1
var entries: [ChatListSearchEntry] = []
var index = 0
@ -3046,11 +3049,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var numberOfGlobalPeers = 0
index = 0
for peer in foundRemotePeers.2 {
if !existingPeerIds.contains(peer.peer.id) {
existingPeerIds.insert(peer.peer.id)
entries.append(.adPeer(peer, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, finalQuery))
index += 1
if !adsHidden {
for peer in foundRemotePeers.2 {
if !existingPeerIds.contains(peer.peer.id) {
existingPeerIds.insert(peer.peer.id)
entries.append(.adPeer(peer, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, finalQuery))
index += 1
}
}
}
@ -3448,6 +3453,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
let previousSelectedMessages = Atomic<Set<EngineMessage.Id>?>(value: nil)
let previousExpandGlobalSearch = Atomic<Bool>(value: false)
let previousAdsHidden = Atomic<Bool>(value: false)
self.searchQueryDisposable = (searchQuery
|> deliverOnMainQueue).startStrict(next: { [weak self, weak listInteraction, weak chatListInteraction] query in
@ -3536,6 +3542,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let strongSelf = self {
let previousSelectedMessageIds = previousSelectedMessages.swap(strongSelf.selectedMessages)
let previousExpandGlobalSearch = previousExpandGlobalSearch.swap(strongSelf.searchStateValue.expandGlobalSearch)
let previousAdsHidden = previousAdsHidden.swap(strongSelf.adsHidden)
var entriesAndFlags = foundItems?.0
@ -3572,8 +3579,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let selectionChanged = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
let expandGlobalSearchChanged = previousExpandGlobalSearch != strongSelf.searchStateValue.expandGlobalSearch
let adsHiddenChanged = previousAdsHidden != strongSelf.adsHidden
let animated = selectionChanged || expandGlobalSearchChanged
let animated = selectionChanged || expandGlobalSearchChanged || adsHiddenChanged
let firstTime = previousEntries == nil
var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags != nil, isEmpty: !isSearching && (entriesAndFlags?.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, requestPeerType: requestPeerType, location: location, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture, location in
interaction.peerContextAction?(message, node, rect, gesture, location)
@ -4909,6 +4917,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.mediaNode.updateSelectedMessages(animated: animated)
}
func removeAds() {
self.adsHidden = true
}
private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) {
self.enqueuedRecentTransitions.append((transition, firstTime))

View File

@ -23,6 +23,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode {
func updateSelectedMessages(animated: Bool)
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
func didBecomeFocused()
func removeAds()
var searchCurrentMessages: [EngineMessage]? { get }
}

View File

@ -210,6 +210,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)?
let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)?
let adButtonAction: ((ASDisplayNode) -> Void)?
let visibilityUpdated: ((Bool) -> Void)?
public let selectable: Bool
@ -254,7 +255,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
animationRenderer: MultiAnimationRenderer? = nil,
storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? = nil,
openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil,
adButtonAction: ((ASDisplayNode) -> Void)? = nil
adButtonAction: ((ASDisplayNode) -> Void)? = nil,
visibilityUpdated: ((Bool) -> Void)? = nil
) {
self.presentationData = presentationData
self.style = style
@ -294,6 +296,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.storyStats = storyStats
self.openStories = openStories
self.adButtonAction = adButtonAction
self.visibilityUpdated = visibilityUpdated
if let index = index {
var letter: String = "#"
@ -538,6 +541,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
)
}
self.statusNode.visibilityRect = self.visibilityStatus == false ? CGRect.zero : CGRect.infinite
self.item?.visibilityUpdated?(self.visibilityStatus)
}
}
}
@ -1799,14 +1804,17 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
adButton = current
} else {
adButton = HighlightableButtonNode()
adButton.setImage(UIImage(bundleImageName: "Components/AdMock"), for: .normal)
strongSelf.addSubnode(adButton)
strongSelf.adButton = adButton
adButton.addTarget(strongSelf, action: #selector(strongSelf.adButtonPressed), forControlEvents: .touchUpInside)
}
adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - 31.0 - 13.0, y: 11.0), size: CGSize(width: 31.0, height: 15.0))
if updatedTheme != nil || adButton.image(for: .normal) == nil {
adButton.setImage(PresentationResourcesChatList.searchAdIcon(item.presentationData.theme, strings: item.presentationData.strings), for: .normal)
}
if let icon = adButton.image(for: .normal) {
adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - icon.size.width - 13.0, y: 11.0), size: icon.size).insetBy(dx: -11.0, dy: -11.0)
}
} else if let adButton = strongSelf.adButton {
strongSelf.adButton = nil
adButton.removeFromSupernode()

View File

@ -308,6 +308,7 @@ private enum PreferencesKeyValues: Int32 {
case botBiometricsState = 39
case businessLinks = 40
case starGifts = 41
case botStorageState = 42
}
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
@ -538,6 +539,13 @@ public struct PreferencesKeys {
key.setInt32(0, value: PreferencesKeyValues.starGifts.rawValue)
return key
}
public static func botStorageState(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8)
key.setInt32(0, value: PreferencesKeyValues.botStorageState.rawValue)
key.setInt64(4, value: peerId.toInt64())
return key
}
}
private enum SharedDataKeyValues: Int32 {

View File

@ -2093,6 +2093,37 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct BotStorageValue: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = String?
fileprivate var id: EnginePeer.Id
fileprivate var storageKey: String
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id, key: String) {
self.id = id
self.storageKey = key
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.botStorageState(peerId: self.id)]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
if let state = view.values[PreferencesKeys.botStorageState(peerId: self.id)]?.get(TelegramBotStorageState.self) {
return state.data[self.storageKey]
} else {
return nil
}
}
}
public struct BusinessChatLinks: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = TelegramBusinessChatLinks?

View File

@ -617,10 +617,11 @@ func _internal_markAdAction(account: Account, opaqueId: Data, media: Bool, fulls
let _ = signal.start()
}
func _internal_markAsSeen(account: Account, opaqueId: Data) -> Signal<Never, NoError> {
return account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
func _internal_markAdAsSeen(account: Account, opaqueId: Data) {
let signal = account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
let _ = signal.start()
}

View File

@ -416,6 +416,86 @@ func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId:
|> switchToLatest
}
private let maxBotStorageSize = 5 * 1024 * 1024
public struct TelegramBotStorageState: Codable, Equatable {
public struct KeyValue: Codable, Equatable {
var key: String
var value: String
}
public var data: [String: String]
public init(
data: [String: String]
) {
self.data = data
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
let values = try container.decode([KeyValue].self, forKey: "data")
var data: [String: String] = [:]
for pair in values {
data[pair.key] = pair.value
}
self.data = data
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
var values: [KeyValue] = []
for (key, value) in self.data {
values.append(KeyValue(key: key, value: value))
}
try container.encode(values, forKey: "data")
}
}
private func _internal_updateBotStorageState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotStorageState?) -> TelegramBotStorageState) -> Signal<Never, BotStorageError> {
return account.postbox.transaction { transaction -> Signal<Never, BotStorageError> in
let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId))?.get(TelegramBotStorageState.self)
let updatedState = update(previousState)
var totalSize = 0
for (_, value) in updatedState.data {
totalSize += value.utf8.count
}
guard totalSize <= maxBotStorageSize else {
return .fail(.quotaExceeded)
}
transaction.setPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId), value: PreferencesEntry(updatedState))
return .never()
}
|> castError(BotStorageError.self)
|> switchToLatest
|> ignoreValues
}
public enum BotStorageError {
case quotaExceeded
}
func _internal_setBotStorageValue(account: Account, peerId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, BotStorageError> {
return _internal_updateBotStorageState(account: account, peerId: peerId, update: { current in
var data = current?.data ?? [:]
if let value {
data[key] = value
} else {
data.removeValue(forKey: key)
}
return TelegramBotStorageState(data: data)
})
}
func _internal_clearBotStorage(account: Account, peerId: EnginePeer.Id) -> Signal<Never, BotStorageError> {
return _internal_updateBotStorageState(account: account, peerId: peerId, update: { _ in
return TelegramBotStorageState(data: [:])
})
}
public struct TelegramBotBiometricsState: Codable, Equatable {
public struct OpaqueToken: Codable, Equatable {
public let publicKey: Data

View File

@ -1520,6 +1520,10 @@ public extension TelegramEngine {
_internal_markAdAction(account: self.account, opaqueId: opaqueId, media: media, fullscreen: fullscreen)
}
public func markAdAsSeen(opaqueId: Data) {
_internal_markAdAsSeen(account: self.account, opaqueId: opaqueId)
}
public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> {
return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in
var result: [EnginePeer.Id] = []

View File

@ -1529,11 +1529,7 @@ public extension TelegramEngine {
public func searchAdPeers(query: String) -> Signal<[AdPeer], NoError> {
return _internal_searchAdPeers(account: self.account, query: query)
}
public func markAsSeen(ad opaqueId: Data) -> Signal<Never, NoError> {
return _internal_markAsSeen(account: self.account, opaqueId: opaqueId)
}
public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id: RequirementToContact], NoError> {
return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds)
}
@ -1673,6 +1669,14 @@ public extension TelegramEngine {
return _internal_botsWithBiometricState(account: self.account)
}
public func setBotStorageValue(peerId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, BotStorageError> {
return _internal_setBotStorageValue(account: self.account, peerId: peerId, key: key, value: value)
}
public func clearBotStorage(peerId: EnginePeer.Id) -> Signal<Never, BotStorageError> {
return _internal_clearBotStorage(account: self.account, peerId: peerId)
}
public func toggleChatManagingBotIsPaused(chatId: EnginePeer.Id) {
let _ = _internal_toggleChatManagingBotIsPaused(account: self.account, chatId: chatId).startStandalone()
}

View File

@ -128,6 +128,8 @@ public enum PresentationResourceKey: Int32 {
case chatListGeneralTopicIcon
case chatListGeneralTopicSmallIcon
case searchAdIcon
case chatTitleLockIcon
case chatTitleMuteIcon

View File

@ -535,4 +535,38 @@ public struct PresentationResourcesChatList {
})
})
}
public static func searchAdIcon(_ theme: PresentationTheme, strings: PresentationStrings) -> UIImage? {
return theme.image(PresentationResourceKey.searchAdIcon.rawValue, { theme in
let titleString = NSAttributedString(string: strings.ChatList_Search_Ad, font: Font.regular(11.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
let stringRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
return generateImage(CGSize(width: floor(stringRect.width) + 18.0, height: 15.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor)
context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: size.height / 2.0).cgPath)
context.fillPath()
context.setFillColor(theme.list.itemAccentColor.cgColor)
let circleSize = CGSize(width: 2.0 - UIScreenPixel, height: 2.0 - UIScreenPixel)
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 3.0 + UIScreenPixel), size: circleSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 7.0 - UIScreenPixel), size: circleSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 10.0), size: circleSize))
let textRect = CGRect(
x: 5.0,
y: (size.height - stringRect.height) / 2.0 - UIScreenPixel,
width: stringRect.width,
height: stringRect.height
)
UIGraphicsPushContext(context)
titleString.draw(in: textRect)
UIGraphicsPopContext()
})
})
}
}

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "admock.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "MockSMS.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load Diff