Various improvements

This commit is contained in:
Ilya Laktyushin 2025-01-10 05:15:00 +04:00
parent 2bf24b2bd9
commit b0511f146e
32 changed files with 720 additions and 393 deletions

View File

@ -13650,3 +13650,20 @@ Sorry for the inconvenience.";
"Story.ViewGift" = "View Gift"; "Story.ViewGift" = "View Gift";
"Camera.OpenChat" = "Open Chat"; "Camera.OpenChat" = "Open Chat";
"Conversation.AddToContactsLong" = "Add to Contacts";
"PeerInfo.PaneRecommendedBots" = "Similar Bots";
"SharedMedia.SimilarChannelCount_1" = "%@ channel";
"SharedMedia.SimilarChannelCount_any" = "%@ channels";
"SharedMedia.SimilarBotCount_1" = "%@ bot";
"SharedMedia.SimilarBotCount_any" = "%@ bots";
"PeerInfo.SimilarBots.ShowMore" = "Show More Bots";
"PeerInfo.SimilarBots.ShowMoreInfo" = "Subscribe to [Telegram Premium]()\nto unlock up to **100** similar bots.";
"Gift.View.Context.Share" = "Share";
"Gift.View.Context.CopyLink" = "Copy Link";
"Gift.View.Context.Transfer" = "Transfer";

View File

@ -1103,9 +1103,11 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: (() -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: (() -> Void)?) -> ViewController
func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: (() -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: (() -> Void)?) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController
func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?)
func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, actionCompleted: (() -> Void)?) -> ViewController func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController

View File

@ -7,6 +7,11 @@ import TelegramUIPreferences
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
public enum StorySharingSubject {
case messages([Message])
case gift(StarGift.UniqueGift)
}
public protocol ShareControllerAccountContext: AnyObject { public protocol ShareControllerAccountContext: AnyObject {
var accountId: AccountRecordId { get } var accountId: AccountRecordId { get }
var accountPeerId: EnginePeer.Id { get } var accountPeerId: EnginePeer.Id { get }

View File

@ -382,17 +382,12 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size)
} }
var hasCloseButton = false let hasCloseButton: Bool
if case .xmasPremiumGift = item.notice { switch item.notice {
hasCloseButton = true case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto:
} else if case .setupBirthday = item.notice {
hasCloseButton = true
} else if case .birthdayPremiumGift = item.notice {
hasCloseButton = true
} else if case .premiumGrace = item.notice {
hasCloseButton = true
} else if case .starsSubscriptionLowBalance = item.notice {
hasCloseButton = true hasCloseButton = true
default:
hasCloseButton = false
} }
if let okButtonLayout, let cancelButtonLayout { if let okButtonLayout, let cancelButtonLayout {

View File

@ -221,25 +221,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
})] })]
} }
var storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)?
if let customSubtitle { if let customSubtitle {
status = .custom(string: NSAttributedString(string: customSubtitle), multiline: false, isActive: false, icon: nil) status = .custom(string: NSAttributedString(string: customSubtitle), multiline: false, isActive: false, icon: nil)
} else if let storyData {
storyStats = (storyData.count, storyData.unseenCount, storyData.hasUnseenCloseFriends)
let text: String
text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count))
status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil)
} }
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
interaction.openPeer(peer, .generic, nil, nil) interaction.openPeer(peer, .generic, nil, nil)
}, disabledAction: { _ in }, disabledAction: { _ in
if case let .peer(peer, _, _) = peer { if case let .peer(peer, _, _) = peer {
interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
} }
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue { if case let .peer(peerValue, _) = peer, let peerValue {
interaction.openStories(peerValue, sourceNode) interaction.openStories(peerValue, sourceNode)
} }
@ -581,15 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
case let .custom(showSelf, sections): case let .custom(showSelf, sections):
if !topPeers.isEmpty { if !topPeers.isEmpty {
var index: Int = 0 var index: Int = 0
if showSelf, let accountPeer {
if let peer = topPeers.first(where: { $0.id == accountPeer.id }) {
let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings)
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf))
existingPeerIds.insert(.peer(peer.id))
}
}
var sectionId: Int = 2 var sectionId: Int = 2
for (title, peerIds, hasActions) in sections { for (title, peerIds, hasActions) in sections {
var allSelected = true var allSelected = true
@ -647,6 +631,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
sectionId += 1 sectionId += 1
} }
if showSelf, let accountPeer {
if let peer = topPeers.first(where: { $0.id == accountPeer.id }) {
let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings)
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf))
existingPeerIds.insert(.peer(peer.id))
}
}
var hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty var hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty
if !sections.isEmpty, let selectionState { if !sections.isEmpty, let selectionState {
var hasNonBirthdayPeers = false var hasNonBirthdayPeers = false

View File

@ -810,6 +810,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
selectionView.handlePan(gestureRecognizer) selectionView.handlePan(gestureRecognizer)
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content { } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content {
selectionView.handlePan(gestureRecognizer) selectionView.handlePan(gestureRecognizer)
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .gift = stickerEntity.content {
selectionView.handlePan(gestureRecognizer)
} else { } else {
var isTrappedInBin = false var isTrappedInBin = false
let scale = 100.0 / selectedEntityView.bounds.size.width let scale = 100.0 / selectedEntityView.bounds.size.width

View File

@ -3096,6 +3096,8 @@ public final class DrawingToolsInteraction {
isAdditional = isAdditionalValue isAdditional = isAdditionalValue
} else if case .message = entity.content { } else if case .message = entity.content {
isMessage = true isMessage = true
} else if case .gift = entity.content {
isMessage = true
} }
} else if entityView.entity is DrawingLinkEntity { } else if entityView.entity is DrawingLinkEntity {
isLink = true isLink = true

View File

@ -664,6 +664,8 @@ public final class ShareController: ViewController {
var fromPublicChannel = false var fromPublicChannel = false
if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
fromPublicChannel = true fromPublicChannel = true
} else if case let .url(link) = self.subject, link.contains("t.me/nft/") {
fromPublicChannel = true
} }
self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in

View File

@ -121,6 +121,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese
subject: .url("https://t.me/addstickers/\(info.shortName)"), subject: .url("https://t.me/addstickers/\(info.shortName)"),
forceExternal: true, forceExternal: true,
shareStory: nil, shareStory: nil,
enqueued: nil,
actionCompleted: { [weak parentNavigationController] in actionCompleted: { [weak parentNavigationController] in
if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View File

@ -13,7 +13,6 @@ import TelegramPresentationData
import ShimmerEffect import ShimmerEffect
import StickerPeekUI import StickerPeekUI
import TextFormat import TextFormat
import Accelerate
final class StickerPackPreviewInteraction { final class StickerPackPreviewInteraction {
var previewedItem: StickerPreviewPeekItem? var previewedItem: StickerPreviewPeekItem?
@ -536,100 +535,3 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
} }
} }
} }
private func getAverageColor(image: UIImage) -> UIColor? {
let blurredWidth = 16
let blurredHeight = 16
let blurredBytesPerRow = blurredWidth * 4
guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else {
return nil
}
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
if let cgImage = image.cgImage {
context.withFlippedContext { c in
c.setFillColor(UIColor.white.cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
context.withFlippedContext { c in
c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
}
var sumR: UInt64 = 0
var sumG: UInt64 = 0
var sumB: UInt64 = 0
var sumA: UInt64 = 0
for y in 0 ..< blurredHeight {
let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
sumB += UInt64(pixel.advanced(by: 0).pointee)
sumG += UInt64(pixel.advanced(by: 1).pointee)
sumR += UInt64(pixel.advanced(by: 2).pointee)
sumA += UInt64(pixel.advanced(by: 3).pointee)
}
}
sumR /= UInt64(blurredWidth * blurredHeight)
sumG /= UInt64(blurredWidth * blurredHeight)
sumB /= UInt64(blurredWidth * blurredHeight)
sumA /= UInt64(blurredWidth * blurredHeight)
sumA = 255
var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0)
if color.lightness > 0.8 {
color = color.withMultipliedBrightnessBy(0.8)
}
return color
}

View File

@ -1137,6 +1137,7 @@ private final class StickerPackContainer: ASDisplayNode {
subject: shareSubject, subject: shareSubject,
forceExternal: false, forceExternal: false,
shareStory: nil, shareStory: nil,
enqueued: nil,
actionCompleted: { [weak parentNavigationController] in actionCompleted: { [weak parentNavigationController] in
if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View File

@ -8,6 +8,7 @@ import MediaResources
import Tuples import Tuples
import ImageBlur import ImageBlur
import FastBlur import FastBlur
import Accelerate
public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? {
if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in
@ -660,3 +661,100 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol
return .single(true) return .single(true)
} }
public func getAverageColor(image: UIImage) -> UIColor? {
let blurredWidth = 16
let blurredHeight = 16
let blurredBytesPerRow = blurredWidth * 4
guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else {
return nil
}
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
if let cgImage = image.cgImage {
context.withFlippedContext { c in
c.setFillColor(UIColor.white.cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
context.withFlippedContext { c in
c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
}
var sumR: UInt64 = 0
var sumG: UInt64 = 0
var sumB: UInt64 = 0
var sumA: UInt64 = 0
for y in 0 ..< blurredHeight {
let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
sumB += UInt64(pixel.advanced(by: 0).pointee)
sumG += UInt64(pixel.advanced(by: 1).pointee)
sumR += UInt64(pixel.advanced(by: 2).pointee)
sumA += UInt64(pixel.advanced(by: 3).pointee)
}
}
sumR /= UInt64(blurredWidth * blurredHeight)
sumG /= UInt64(blurredWidth * blurredHeight)
sumB /= UInt64(blurredWidth * blurredHeight)
sumA /= UInt64(blurredWidth * blurredHeight)
sumA = 255
var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0)
if color.lightness > 0.8 {
color = color.withMultipliedBrightnessBy(0.8)
}
return color
}

View File

@ -138,6 +138,7 @@ public struct Namespaces {
public static let starsReactionDefaultToPrivate: Int8 = 41 public static let starsReactionDefaultToPrivate: Int8 = 41
public static let cachedPremiumGiftCodeOptions: Int8 = 42 public static let cachedPremiumGiftCodeOptions: Int8 = 42
public static let cachedProfileGifts: Int8 = 43 public static let cachedProfileGifts: Int8 = 43
public static let recommendedBots: Int8 = 44
} }
public struct UnorderedItemList { public struct UnorderedItemList {

View File

@ -0,0 +1,130 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
final class CachedRecommendedBots: Codable {
public let peerIds: [EnginePeer.Id]
public let count: Int32
public let timestamp: Int32?
public init(peerIds: [EnginePeer.Id], count: Int32, timestamp: Int32?) {
self.peerIds = peerIds
self.count = count
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = try container.decode([Int64].self, forKey: "l").map(EnginePeer.Id.init)
self.count = try container.decodeIfPresent(Int32.self, forKey: "c") ?? 0
self.timestamp = try container.decodeIfPresent(Int32.self, forKey: "ts")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l")
try container.encode(self.count, forKey: "c")
try container.encodeIfPresent(self.timestamp, forKey: "ts")
}
}
private func entryId(peerId: EnginePeer.Id?) -> ItemCacheEntryId {
let cacheKey = ValueBoxKey(length: 8)
if let peerId {
cacheKey.setInt64(0, value: peerId.toInt64())
} else {
cacheKey.setInt64(0, value: 0)
}
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedBots, key: cacheKey)
}
func _internal_requestRecommendedBots(account: Account, peerId: EnginePeer.Id, forceUpdate: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Peer?, Bool) in
guard let user = transaction.getPeer(peerId) as? TelegramUser, let _ = user.botInfo else {
return (nil, false)
}
if let entry = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedRecommendedBots.self), !entry.peerIds.isEmpty && !forceUpdate {
return (nil, false)
} else {
return (user, true)
}
}
|> mapToSignal { user, shouldUpdate in
guard shouldUpdate, let user, let inputUser = apiInputUser(user) else {
return .complete()
}
return account.network.request(Api.functions.bots.getBotRecommendations(bot: inputUser))
|> retryRequest
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> [EnginePeer] in
let users: [Api.User]
let parsedPeers: AccumulatedPeers
var count: Int32
switch result {
case let .users(apiUsers):
users = apiUsers
count = Int32(apiUsers.count)
case let .usersSlice(apiCount, apiUsers):
users = apiUsers
count = apiCount
}
parsedPeers = AccumulatedPeers(users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
let peers = users.map { EnginePeer(TelegramUser(user: $0)) }
if let entry = CodableEntry(CachedRecommendedBots(peerIds: peers.map(\.id), count: count, timestamp: Int32(Date().timeIntervalSince1970))) {
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
}
return peers
}
|> ignoreValues
}
}
}
public struct RecommendedBots: Equatable {
public var bots: [EnginePeer]
public var count: Int32
public init(bots: [EnginePeer], count: Int32) {
self.bots = bots
self.count = count
}
}
func _internal_recommendedBotPeerIds(account: Account, peerId: EnginePeer.Id) -> Signal<[EnginePeer.Id]?, NoError> {
let key = PostboxViewKey.cachedItem(entryId(peerId: peerId))
return account.postbox.combinedView(keys: [key])
|> mapToSignal { views -> Signal<[EnginePeer.Id]?, NoError> in
guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self), !cachedBots.peerIds.isEmpty else {
return .single(nil)
}
return .single(cachedBots.peerIds)
}
}
func _internal_recommendedBots(account: Account, peerId: EnginePeer.Id) -> Signal<RecommendedBots?, NoError> {
let key = PostboxViewKey.cachedItem(entryId(peerId: peerId))
return account.postbox.combinedView(keys: [key])
|> mapToSignal { views -> Signal<RecommendedBots?, NoError> in
guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self) else {
return .single(nil)
}
if cachedBots.peerIds.isEmpty {
return .single(nil)
}
return account.postbox.transaction { transaction -> RecommendedBots? in
var bots: [EnginePeer] = []
for peerId in cachedBots.peerIds {
if let peer = transaction.getPeer(peerId) {
bots.append(EnginePeer(peer))
}
}
return RecommendedBots(bots: bots, count: cachedBots.count)
}
}
}

View File

@ -1454,7 +1454,15 @@ public extension TelegramEngine {
public func requestRecommendedAppsIfNeeded() -> Signal<Never, NoError> { public func requestRecommendedAppsIfNeeded() -> Signal<Never, NoError> {
return _internal_requestRecommendedApps(account: self.account, forceUpdate: false) return _internal_requestRecommendedApps(account: self.account, forceUpdate: false)
} }
public func recommendedBots(peerId: EnginePeer.Id) -> Signal<RecommendedBots?, NoError> {
return _internal_recommendedBots(account: self.account, peerId: peerId)
}
public func requestRecommendedBots(peerId: EnginePeer.Id, forceUpdate: Bool = false) -> Signal<Never, NoError> {
return _internal_requestRecommendedBots(account: self.account, peerId: peerId, forceUpdate: forceUpdate)
}
public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id], NoError> { public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id], NoError> {
return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds) return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds)
} }

View File

@ -357,6 +357,16 @@ public final class ButtonComponent: Component {
self.cornerRadius = cornerRadius self.cornerRadius = cornerRadius
self.isShimmering = isShimmering self.isShimmering = isShimmering
} }
public func withIsShimmering(_ isShimmering: Bool) -> Background {
return Background(
color: self.color,
foreground: self.foreground,
pressedColor: self.pressedColor,
cornerRadius: self.cornerRadius,
isShimmering: isShimmering
)
}
} }
public let background: Background public let background: Background

View File

@ -1847,6 +1847,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var transferGiftImpl: (() -> Void)? var transferGiftImpl: (() -> Void)?
var showAttributeInfoImpl: ((Any, Float) -> Void)? var showAttributeInfoImpl: ((Any, Float) -> Void)?
var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
var shareGiftImpl: (() -> Void)?
var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)?
var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)?
@ -2140,6 +2141,74 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} }
} }
shareGiftImpl = { [weak self] in
guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
return
}
let link = "https://t.me/nft/\(gift.slug)"
let shareController = context.sharedContext.makeShareController(
context: context,
subject: .url(link),
forceExternal: false,
shareStory: shareStory,
enqueued: { peerIds, _ in
let _ = (context.engine.data.get(
EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
let peers = peerList.compactMap { $0 }
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
var savedMessages = false
if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId {
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
}
self?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if savedMessages, action == .info {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer else {
return
}
openPeerImpl?(peer)
Queue.mainQueue().after(1.0) {
self?.dismiss(animated: false, completion: nil)
}
})
}
return false
}, additionalView: nil), in: .current)
})
},
actionCompleted: { [weak self] in
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
)
self.present(shareController, in: .window(.root))
}
viewUpgradedImpl = { [weak self] messageId in viewUpgradedImpl = { [weak self] messageId in
guard let self, let navigationController = self.navigationController as? NavigationController else { guard let self, let navigationController = self.navigationController as? NavigationController else {
return return
@ -2178,13 +2247,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} }
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let link = "https://t.me/nft/\(gift.slug)" let link = "https://t.me/nft/\(gift.slug)"
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in }, action: { [weak self] c, _ in
c?.dismiss(completion: nil) c?.dismiss(completion: nil)
@ -2198,28 +2264,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}))) })))
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in }, action: { c, _ in
c?.dismiss(completion: nil) c?.dismiss(completion: nil)
guard let self else { shareGiftImpl?()
return
}
let shareController = context.sharedContext.makeShareController(
context: context,
subject: .url(link),
forceExternal: false,
shareStory: shareStory,
actionCompleted: { [weak self] in
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
)
self.present(shareController, in: .window(.root))
}))) })))
if let _ = arguments.transferStars { if let _ = arguments.transferStars {
items.append(.action(ContextMenuActionItem(text: "Transfer", icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Transfer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in }, action: { c, _ in
c?.dismiss(completion: nil) c?.dismiss(completion: nil)

View File

@ -88,6 +88,7 @@ public final class DrawingMessageRenderer {
private let isOverlay: Bool private let isOverlay: Bool
private let isLink: Bool private let isLink: Bool
private let isGift: Bool private let isGift: Bool
private let wallpaperColor: UIColor?
private let messagesContainerNode: ASDisplayNode private let messagesContainerNode: ASDisplayNode
private var avatarHeaderNode: ListViewItemHeaderNode? private var avatarHeaderNode: ListViewItemHeaderNode?
@ -99,7 +100,8 @@ public final class DrawingMessageRenderer {
isNight: Bool = false, isNight: Bool = false,
isOverlay: Bool = false, isOverlay: Bool = false,
isLink: Bool = false, isLink: Bool = false,
isGift: Bool = false isGift: Bool = false,
wallpaperColor: UIColor? = nil
) { ) {
self.context = context self.context = context
self.messages = messages self.messages = messages
@ -107,6 +109,7 @@ public final class DrawingMessageRenderer {
self.isOverlay = isOverlay self.isOverlay = isOverlay
self.isLink = isLink self.isLink = isLink
self.isGift = isGift self.isGift = isGift
self.wallpaperColor = wallpaperColor
self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true self.messagesContainerNode.clipsToBounds = true
@ -169,14 +172,19 @@ public final class DrawingMessageRenderer {
} }
} }
} }
var borderColor: UIColor?
if self.isGift && !self.isOverlay, let wallpaperColor = self.wallpaperColor {
borderColor = wallpaperColor.withMultiplied(hue: 1.0, saturation: 1.5, brightness: self.isNight ? 1.6 : 0.7).withAlphaComponent(0.6)
}
self.generate(size: size) { image in self.generate(size: size, borderColor: borderColor) { image in
completion(size, image, mediaRect) completion(size, image, mediaRect)
} }
}) })
} }
private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { private func generate(size: CGSize, borderColor: UIColor? = nil, completion: @escaping (UIImage) -> Void) {
UIGraphicsBeginImageContextWithOptions(size, false, 3.0) UIGraphicsBeginImageContextWithOptions(size, false, 3.0)
self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext() let img = UIGraphicsGetImageFromCurrentImageContext()
@ -184,6 +192,11 @@ public final class DrawingMessageRenderer {
let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
if let borderColor {
context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: 6.0, y: 12.0), size: CGSize(width: size.width - 6.0, height: size.height - 13.0)), cornerWidth: 70.0, cornerHeight: 70.0, transform: nil))
context.setFillColor(borderColor.cgColor)
context.fillPath()
}
if let cgImage = img?.cgImage { if let cgImage = img?.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
} }
@ -351,14 +364,16 @@ public final class DrawingMessageRenderer {
messages: [Message], messages: [Message],
parentView: UIView, parentView: UIView,
isLink: Bool = false, isLink: Bool = false,
isGift: Bool = false isGift: Bool = false,
wallpaperDayColor: UIColor? = nil,
wallpaperNightColor: UIColor? = nil
) { ) {
self.context = context self.context = context
self.messages = messages self.messages = messages
self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift) self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperDayColor)
self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift) self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperNightColor)
self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift) self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift, wallpaperColor: nil)
parentView.addSubview(self.dayContainerNode.view) parentView.addSubview(self.dayContainerNode.view)
parentView.addSubview(self.nightContainerNode.view) parentView.addSubview(self.nightContainerNode.view)

View File

@ -308,7 +308,15 @@ public final class MediaEditor {
return self.renderer.finalRenderedImage(mirror: mirror) return self.renderer.finalRenderedImage(mirror: mirror)
} }
private var wallpapers: ((day: UIImage, night: UIImage?))? private var wallpapersValue: ((day: UIImage, night: UIImage?))? {
didSet {
self.wallpapersPromise.set(.single(self.wallpapersValue))
}
}
private let wallpapersPromise = Promise<(day: UIImage, night: UIImage?)?>()
public var wallpapers: Signal<((day: UIImage, night: UIImage?))?, NoError> {
return self.wallpapersPromise.get()
}
private struct PlaybackState: Equatable { private struct PlaybackState: Equatable {
let duration: Double let duration: Double
@ -871,10 +879,13 @@ public final class MediaEditor {
let textureSource = UniversalTextureSource(renderTarget: renderTarget) let textureSource = UniversalTextureSource(renderTarget: renderTarget)
if case .message = self.self.subject { switch self.subject {
case .message, .gift:
if let image = textureSourceResult.image { if let image = textureSourceResult.image {
self.wallpapers = (image, textureSourceResult.nightImage ?? image) self.wallpapersValue = (image, textureSourceResult.nightImage ?? image)
} }
default:
break
} }
self.player = textureSourceResult.player self.player = textureSourceResult.player
@ -1240,7 +1251,7 @@ public final class MediaEditor {
return values.withUpdatedNightTheme(nightTheme) return values.withUpdatedNightTheme(nightTheme)
} }
guard let (dayImage, nightImage) = self.wallpapers, let nightImage else { guard let (dayImage, nightImage) = self.wallpapersValue, let nightImage else {
return return
} }

View File

@ -3388,53 +3388,83 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
messageFile = nil messageFile = nil
} }
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift) let wallpaperColors: Signal<(UIColor?, UIColor?), NoError>
renderer.render(completion: { result in if let subject = self.subject, case .gift = subject {
if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in wallpaperColors = self.mediaEditorPromise.get()
if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { |> mapToSignal { mediaEditor in
return true if let mediaEditor {
} else { return mediaEditor.wallpapers
return false |> filter {
} $0 != nil
}) as? DrawingStickerEntityView { }
existingEntityView.isNightTheme = isNightTheme |> take(1)
let messageEntity = existingEntityView.entity as! DrawingStickerEntity |> map { result in
messageEntity.renderImage = result.dayImage if let (dayImage, nightImage) = result {
messageEntity.secondaryRenderImage = result.nightImage return (getAverageColor(image: dayImage), nightImage.flatMap { getAverageColor(image: $0) })
messageEntity.overlayRenderImage = result.overlayImage }
existingEntityView.update(animated: false) return (nil, nil)
} else {
var content: DrawingStickerEntity.Content
var position: CGPoint
switch effectiveSubject {
case let .message(messageIds):
content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)
position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0)
case let .gift(gift):
content = .gift(gift, result.size)
position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
default:
fatalError()
}
let messageEntity = DrawingStickerEntity(content: content)
messageEntity.renderImage = result.dayImage
messageEntity.secondaryRenderImage = result.nightImage
messageEntity.overlayRenderImage = result.overlayImage
messageEntity.referenceDrawingSize = storyDimensions
messageEntity.position = position
let fraction = max(result.size.width, result.size.height) / 353.0
messageEntity.scale = min(6.0, 3.3 * fraction)
if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView {
if isNightTheme {
entityView.isNightTheme = true
} }
} }
return .complete()
} }
self.readyValue.set(.single(true)) } else {
wallpaperColors = .single((nil, nil))
}
let _ = (wallpaperColors
|> deliverOnMainQueue).start(next: { [weak self] wallpaperColors in
guard let self else {
return
}
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1)
renderer.render(completion: { result in
if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in
if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content {
return true
} else {
return false
}
}) as? DrawingStickerEntityView {
existingEntityView.isNightTheme = isNightTheme
let messageEntity = existingEntityView.entity as! DrawingStickerEntity
messageEntity.renderImage = result.dayImage
messageEntity.secondaryRenderImage = result.nightImage
messageEntity.overlayRenderImage = result.overlayImage
existingEntityView.update(animated: false)
} else {
var content: DrawingStickerEntity.Content
var position: CGPoint
switch effectiveSubject {
case let .message(messageIds):
content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)
position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0)
case let .gift(gift):
content = .gift(gift, result.size)
position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
default:
fatalError()
}
let messageEntity = DrawingStickerEntity(content: content)
messageEntity.renderImage = result.dayImage
messageEntity.secondaryRenderImage = result.nightImage
messageEntity.overlayRenderImage = result.overlayImage
messageEntity.referenceDrawingSize = storyDimensions
messageEntity.position = position
let fraction = max(result.size.width, result.size.height) / 353.0
messageEntity.scale = min(6.0, 3.3 * fraction)
if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView {
if isNightTheme {
entityView.isNightTheme = true
}
}
}
self.readyValue.set(.single(true))
})
}) })
}) })
default: default:

View File

@ -20,7 +20,8 @@ public enum PeerInfoPaneKey: Int32 {
case links case links
case gifs case gifs
case groupsInCommon case groupsInCommon
case recommended case similarChannels
case similarBots
} }
public struct PeerInfoStatusData: Equatable { public struct PeerInfoStatusData: Equatable {

View File

@ -20,29 +20,29 @@ import Markdown
import SolidRoundedButtonNode import SolidRoundedButtonNode
import PeerInfoPaneNode import PeerInfoPaneNode
private struct RecommendedChannelsListTransaction { private struct RecommendedPeersListTransaction {
let deletions: [ListViewDeleteItem] let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem] let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem] let updates: [ListViewUpdateItem]
let animated: Bool let animated: Bool
} }
private enum RecommendedChannelsListEntryStableId: Hashable { private enum RecommendedPeersListEntryStableId: Hashable {
case addMember case addMember
case peer(PeerId) case peer(PeerId)
} }
private enum RecommendedChannelsListEntry: Comparable, Identifiable { private enum RecommendedPeersListEntry: Comparable, Identifiable {
case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32) case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32)
var stableId: RecommendedChannelsListEntryStableId { var stableId: RecommendedPeersListEntryStableId {
switch self { switch self {
case let .peer(_, _, peer, _): case let .peer(_, _, peer, _):
return .peer(peer.id) return .peer(peer.id)
} }
} }
static func ==(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { static func ==(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool {
switch lhs { switch lhs {
case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers): case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers):
if case let .peer(rhsTheme, rhsIndex, rhsPeer, rhsSubscribers) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsSubscribers == rhsSubscribers { if case let .peer(rhsTheme, rhsIndex, rhsPeer, rhsSubscribers) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsSubscribers == rhsSubscribers {
@ -53,7 +53,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
} }
} }
static func <(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { static func <(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool {
switch lhs { switch lhs {
case let .peer(_, lhsIndex, _, _): case let .peer(_, lhsIndex, _, _):
switch rhs { switch rhs {
@ -66,8 +66,15 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
switch self { switch self {
case let .peer(_, _, peer, subscribers): case let .peer(_, _, peer, subscribers):
let subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers) let text: ItemListPeerItemText
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: .text(subtitle, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { if subscribers > 0 {
text = .text(presentationData.strings.Conversation_StatusSubscribers(subscribers), .secondary)
} else if let addressName = peer.addressName {
text = .text("@\(addressName)", .secondary)
} else {
text = .none
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
action(peer) action(peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in }, removePeer: { _ in
@ -78,19 +85,29 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
} }
} }
private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry], to toEntries: [RecommendedChannelsListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedChannelsListTransaction { private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction {
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, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
return RecommendedChannelsListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) return RecommendedPeersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count)
} }
private let channelsLimit: Int32 = 8 private protocol RecommendedPeers {
}
final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode { extension RecommendedChannels: RecommendedPeers {
}
extension RecommendedBots: RecommendedPeers {
}
final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext private let context: AccountContext
private let chatControllerInteraction: ChatControllerInteraction private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
@ -98,9 +115,9 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
weak var parentController: ViewController? weak var parentController: ViewController?
private let listNode: ListView private let listNode: ListView
private var currentEntries: [RecommendedChannelsListEntry] = [] private var currentEntries: [RecommendedPeersListEntry] = []
private var currentState: (RecommendedChannels?, Bool)? private var enqueuedTransactions: [RecommendedPeersListTransaction] = []
private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] private var currentState: (RecommendedPeers?, Bool)?
private var unlockBackground: UIImageView? private var unlockBackground: UIImageView?
private var unlockText: ComponentView<Empty>? private var unlockText: ComponentView<Empty>?
@ -145,32 +162,35 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.listNode.preloadPages = true self.listNode.preloadPages = true
self.addSubnode(self.listNode) self.addSubnode(self.listNode)
let signal: Signal<RecommendedPeers?, NoError>
if peerId.namespace == Namespaces.Peer.CloudUser {
signal = context.engine.peers.recommendedBots(peerId: peerId)
|> map {
$0 as RecommendedPeers?
}
} else {
signal = context.engine.peers.recommendedChannels(peerId: peerId)
|> map {
$0 as RecommendedPeers?
}
}
self.disposable = (combineLatest(queue: .mainQueue(), self.disposable = (combineLatest(queue: .mainQueue(),
self.presentationDataPromise.get(), self.presentationDataPromise.get(),
context.engine.peers.recommendedChannels(peerId: peerId), signal,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in |> map { peer -> Bool in
return peer?.isPremium ?? false return peer?.isPremium ?? false
} }
) )
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedChannels, isPremium in |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedPeers, isPremium in
guard let strongSelf = self else { guard let self else {
return return
} }
strongSelf.currentState = (recommendedChannels, isPremium) self.currentState = (recommendedPeers, isPremium)
strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData)
}) })
self.statusPromise.set(context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId)
)
|> map { count -> PeerInfoStatusData? in
if let count {
return PeerInfoStatusData(text: presentationData.strings.Conversation_StatusSubscribers(Int32(count)), isActivity: true, key: .recommended)
}
return nil
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
if let self { if let self {
self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring)) self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring))
@ -215,8 +235,8 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.listNode.scrollEnabled = !isScrollingLockedAtTop self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout, let (recommendedChannels, isPremium) = self.currentState { if isFirstLayout, let (recommendedPeers, isPremium) = self.currentState {
self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData)
} }
} }
@ -225,8 +245,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.chatControllerInteraction.navigationController()?.pushViewController(controller) self.chatControllerInteraction.navigationController()?.pushViewController(controller)
} }
private func updateState(recommendedPeers: RecommendedPeers?, isPremium: Bool, presentationData: PresentationData) {
if let recommendedChannels = recommendedPeers as? RecommendedChannels {
self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData)
} else if let recommendedBots = recommendedPeers as? RecommendedBots {
self.updateState(recommendedBots: recommendedBots, isPremium: isPremium, presentationData: presentationData)
}
}
private func updateState(recommendedChannels: RecommendedChannels?, isPremium: Bool, presentationData: PresentationData) { private func updateState(recommendedChannels: RecommendedChannels?, isPremium: Bool, presentationData: PresentationData) {
var entries: [RecommendedChannelsListEntry] = [] var entries: [RecommendedPeersListEntry] = []
if let channels = recommendedChannels?.channels { if let channels = recommendedChannels?.channels {
for channel in channels { for channel in channels {
@ -243,6 +271,42 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.currentEntries = entries self.currentEntries = entries
self.enqueuedTransactions.append(transaction) self.enqueuedTransactions.append(transaction)
self.dequeueTransaction() self.dequeueTransaction()
if let recommendedChannels {
self.statusPromise.set(.single(
PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarChannelCount(recommendedChannels.count), isActivity: true, key: .similarChannels)
))
}
}
private func updateState(recommendedBots: RecommendedBots?, isPremium: Bool, presentationData: PresentationData) {
var entries: [RecommendedPeersListEntry] = []
if let bots = recommendedBots?.bots {
for bot in bots {
var subscriberCount: Int32 = 0
if case let .user(user) = bot {
subscriberCount = user.subscriberCount ?? 0
}
entries.append(.peer(theme: presentationData.theme, index: entries.count, peer: bot, subscribers: subscriberCount))
}
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in
self?.chatControllerInteraction.openPeer(peer, .info(nil), nil, .default)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(true, peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
if let recommendedBots {
self.statusPromise.set(.single(
PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarBotCount(recommendedBots.count), isActivity: true, key: .similarBots)
))
}
} }
private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) { private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) {
@ -278,6 +342,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.view.addSubview(unlockBackground) self.view.addSubview(unlockBackground)
self.unlockBackground = unlockBackground self.unlockBackground = unlockBackground
} }
var isBots = false
if let (state, _) = self.currentState, state is RecommendedBots {
isBots = true
}
if let current = self.unlockButton { if let current = self.unlockButton {
unlockButton = current unlockButton = current
@ -289,7 +358,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
unlockButton.animationLoopTime = 2.5 unlockButton.animationLoopTime = 2.5
unlockButton.animation = "premium_unlock" unlockButton.animation = "premium_unlock"
unlockButton.iconPosition = .right unlockButton.iconPosition = .right
unlockButton.title = presentationData.strings.Channel_SimilarChannels_ShowMore unlockButton.title = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore
unlockButton.pressed = { [weak self] in unlockButton.pressed = { [weak self] in
self?.unlockPressed() self?.unlockPressed()
@ -320,7 +389,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( MultilineTextComponent(
text: .markdown(text: presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes), text: .markdown(text: isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMoreInfo : presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2 lineSpacing: 0.2

View File

@ -1008,6 +1008,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon = nil groupsInCommon = nil
} }
let recommendedBots: Signal<RecommendedBots?, NoError>
if case .bot = kind {
recommendedBots = context.engine.peers.recommendedBots(peerId: userPeerId)
} else {
recommendedBots = .single(nil)
}
let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError>
let profileGiftsContext: ProfileGiftsContext? let profileGiftsContext: ProfileGiftsContext?
if case .user = kind { if case .user = kind {
@ -1309,6 +1316,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
status, status,
hasStories, hasStories,
hasStoryArchive, hasStoryArchive,
recommendedBots,
accountIsPremium, accountIsPremium,
savedMessagesPeer, savedMessagesPeer,
hasSavedMessagesChats, hasSavedMessagesChats,
@ -1322,7 +1330,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
premiumGiftOptions, premiumGiftOptions,
webAppPermissions webAppPermissions
) )
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in
var availablePanes = availablePanes var availablePanes = availablePanes
if isMyProfile { if isMyProfile {
availablePanes?.insert(.stories, at: 0) availablePanes?.insert(.stories, at: 0)
@ -1373,6 +1381,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.botPreview, at: 0) availablePanes?.insert(.botPreview, at: 0)
} }
} }
if let recommendedBots, recommendedBots.count > 0 {
availablePanes?.append(.similarBots)
}
} else { } else {
availablePanes = nil availablePanes = nil
} }
@ -1574,7 +1586,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.stories, at: 0) availablePanes?.insert(.stories, at: 0)
} }
if let recommendedChannels, !recommendedChannels.channels.isEmpty { if let recommendedChannels, !recommendedChannels.channels.isEmpty {
availablePanes?.append(.recommended) availablePanes?.append(.similarChannels)
} }
if case .peer = chatLocation { if case .peer = chatLocation {

View File

@ -545,8 +545,8 @@ private final class PeerInfoPendingPane {
} else { } else {
preconditionFailure() preconditionFailure()
} }
case .recommended: case .similarChannels, .similarBots:
paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) paneNode = PeerInfoRecommendedPeersPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction)
case .savedMessagesChats: case .savedMessagesChats:
paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController)
case .savedMessages: case .savedMessages:
@ -1201,8 +1201,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_PaneGroups title = presentationData.strings.PeerInfo_PaneGroups
case .members: case .members:
title = presentationData.strings.PeerInfo_PaneMembers title = presentationData.strings.PeerInfo_PaneMembers
case .recommended: case .similarChannels:
title = presentationData.strings.PeerInfo_PaneRecommended title = presentationData.strings.PeerInfo_PaneRecommended
case .similarBots:
title = presentationData.strings.PeerInfo_PaneRecommendedBots
case .savedMessagesChats: case .savedMessagesChats:
title = presentationData.strings.DialogList_TabTitle title = presentationData.strings.DialogList_TabTitle
case .savedMessages: case .savedMessages:

View File

@ -4824,6 +4824,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}) })
} }
if peerId.namespace == Namespaces.Peer.CloudUser {
let _ = context.engine.peers.requestRecommendedBots(peerId: peerId, forceUpdate: true).startStandalone()
}
if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudUser { if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudUser {
self.storiesReady.set(false) self.storiesReady.set(false)
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
@ -12641,7 +12645,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
override public func loadDisplayNode() { override public func loadDisplayNode() {
var initialPaneKey: PeerInfoPaneKey? var initialPaneKey: PeerInfoPaneKey?
if self.switchToRecommendedChannels { if self.switchToRecommendedChannels {
initialPaneKey = .recommended initialPaneKey = .similarChannels
} else if self.switchToGifts { } else if self.switchToGifts {
initialPaneKey = .gifts initialPaneKey = .gifts
} }

View File

@ -249,6 +249,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return .never() return .never()
} }
return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo)
},
shareStory: { [weak self] in
guard let self, case let .unique(uniqueGift) = product.gift, let parentController = self.parentController else {
return
}
Queue.mainQueue().after(0.15) {
let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: parentController)
parentController.push(controller)
}
} }
) )
self.parentController?.push(controller) self.parentController?.push(controller)

View File

@ -1,108 +0,0 @@
import Foundation
import UIKit
import Postbox
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
import SafariServices
import MobileCoreServices
import Intents
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
import TextFormat
import TelegramBaseController
import AccountContext
import TelegramStringFormatting
import PresentationDataUtils
import UndoUI
import PeerInfoUI
import AppBundle
import LocalizedPeerData
import ChatInterfaceState
import ChatControllerInteraction
import StoryContainerScreen
import SaveToCameraRoll
import MediaEditorScreen
enum StorySharingSubject {
case messages([Message])
case gift(StarGift.UniqueGift)
}
extension ChatControllerImpl {
func openStorySharing(subject: StorySharingSubject) {
let context = self.context
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {
case let .messages(messages):
editorSubject = .single(.message(messages.map { $0.id }))
case let .gift(gift):
editorSubject = .single(.gift(gift))
}
let externalState = MediaEditorTransitionOutExternalState(
storyTarget: nil,
isForcedTarget: false,
isPeerArchived: false,
transitionOut: nil
)
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
subject: editorSubject,
transitionIn: nil,
transitionOut: { _, _ in
return nil
},
completion: { [weak self] result, commit in
guard let self else {
return
}
let targetPeerId: EnginePeer.Id
let target: Stories.PendingTarget
if let sendAsPeerId = result.options.sendAsPeerId {
target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId
} else {
target = .myStories
targetPeerId = self.context.account.peerId
}
externalState.storyTarget = target
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let text: String
if case .channel = peer {
text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string
} else {
text = self.presentationData.strings.Story_MessageReposted_Personal
}
Queue.mainQueue().after(0.25) {
self.present(UndoOverlayController(
presentationData: self.presentationData,
content: .forward(savedMessages: false, text: text),
elevatedLayout: false,
action: { _ in return false }
), in: .current)
Queue.mainQueue().after(0.1) {
self.chatDisplayNode.hapticFeedback.success()
}
}
})
}
)
self.push(controller)
}
}

View File

@ -1201,7 +1201,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message), shareStory: { [weak self] in let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message), shareStory: { [weak self] in
if let self, case let .starGiftUnique(gift, _, _, _, _, _, _) = action.action, case let .unique(uniqueGift) = gift { if let self, case let .starGiftUnique(gift, _, _, _, _, _, _) = action.action, case let .unique(uniqueGift) = gift {
Queue.mainQueue().after(0.15) { Queue.mainQueue().after(0.15) {
self.openStorySharing(subject: .gift(uniqueGift)) let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self)
self.push(controller)
} }
} }
}) })

View File

@ -134,7 +134,8 @@ extension ChatControllerImpl {
return return
} }
Queue.mainQueue().after(0.15) { Queue.mainQueue().after(0.15) {
self.openStorySharing(subject: .messages(messages)) let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self)
self.push(controller)
} }
} }
} }

View File

@ -2821,12 +2821,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
for entry in historyView.filteredEntries { for entry in historyView.filteredEntries {
switch entry { switch entry {
case let .MessageEntry(message, _, _, _, _, _): case let .MessageEntry(message, _, _, _, _, _):
var hasAction = false
for media in message.media {
if let _ = media as? TelegramMediaAction {
hasAction = true
}
}
if let _ = message.inlineBotAttribute { if let _ = message.inlineBotAttribute {
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
if visibleBusinessBotMessageIdValue < message.id { if visibleBusinessBotMessageIdValue < message.id {
@ -2836,22 +2830,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
visibleBusinessBotMessageId = message.id visibleBusinessBotMessageId = message.id
} }
} }
if !hasAction { switch message.id.peerId.namespace {
switch message.id.peerId.namespace { case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: messageIdsWithPossibleReactions.append(message.id)
messageIdsWithPossibleReactions.append(message.id) default:
default: break
break
}
} }
case let .MessageGroupEntry(_, messages, _): case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _, _) in messages { for (message, _, _, _, _) in messages {
var hasAction = false
for media in message.media {
if let _ = media as? TelegramMediaAction {
hasAction = true
}
}
if let _ = message.inlineBotAttribute { if let _ = message.inlineBotAttribute {
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
if visibleBusinessBotMessageIdValue < message.id { if visibleBusinessBotMessageIdValue < message.id {
@ -2861,13 +2847,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
visibleBusinessBotMessageId = message.id visibleBusinessBotMessageId = message.id
} }
} }
if !hasAction { switch message.id.peerId.namespace {
switch message.id.peerId.namespace { case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: messageIdsWithPossibleReactions.append(message.id)
messageIdsWithPossibleReactions.append(message.id) default:
default: break
break
}
} }
} }
default: default:

View File

@ -17,7 +17,7 @@ import AccountContext
private enum ChatReportPeerTitleButton: Equatable { private enum ChatReportPeerTitleButton: Equatable {
case block case block
case addContact(String?) case addContact(String?, Bool)
case shareMyPhoneNumber case shareMyPhoneNumber
case reportSpam case reportSpam
case reportUserSpam case reportUserSpam
@ -30,11 +30,15 @@ private enum ChatReportPeerTitleButton: Equatable {
switch self { switch self {
case .block: case .block:
return strings.Conversation_BlockUser return strings.Conversation_BlockUser
case let .addContact(name): case let .addContact(name, long):
if let name = name { if let name = name {
return strings.Conversation_AddNameToContacts(name).string return strings.Conversation_AddNameToContacts(name).string
} else { } else {
return strings.Conversation_AddToContacts if long {
return strings.Conversation_AddToContactsLong
} else {
return strings.Conversation_AddToContacts
}
} }
case .shareMyPhoneNumber: case .shareMyPhoneNumber:
return strings.Conversation_ShareMyPhoneNumber return strings.Conversation_ShareMyPhoneNumber
@ -76,9 +80,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport
} }
} }
if buttons.isEmpty, let phone = peer.phone, !phone.isEmpty { if buttons.isEmpty, let phone = peer.phone, !phone.isEmpty {
buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle)) buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle, buttons.isEmpty))
} else { } else {
buttons.append(.addContact(nil)) buttons.append(.addContact(nil, buttons.isEmpty))
} }
} else { } else {
if peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.canReport) { if peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.canReport) {

View File

@ -2931,6 +2931,75 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory) return GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory)
} }
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {
case let .messages(messages):
editorSubject = .single(.message(messages.map { $0.id }))
case let .gift(gift):
editorSubject = .single(.gift(gift))
}
let externalState = MediaEditorTransitionOutExternalState(
storyTarget: nil,
isForcedTarget: false,
isPeerArchived: false,
transitionOut: nil
)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
subject: editorSubject,
transitionIn: nil,
transitionOut: { _, _ in
return nil
},
completion: { [weak parentController] result, commit in
let targetPeerId: EnginePeer.Id
let target: Stories.PendingTarget
if let sendAsPeerId = result.options.sendAsPeerId {
target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId
} else {
target = .myStories
targetPeerId = context.account.peerId
}
externalState.storyTarget = target
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer else {
return
}
let text: String
if case .channel = peer {
text = presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string
} else {
text = presentationData.strings.Story_MessageReposted_Personal
}
Queue.mainQueue().after(0.25) {
parentController?.present(UndoOverlayController(
presentationData: presentationData,
content: .forward(savedMessages: false, text: text),
elevatedLayout: false,
action: { _ in return false }
), in: .current)
Queue.mainQueue().after(0.1) {
HapticFeedback().success()
}
}
})
}
)
return controller
}
public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) { public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) {
let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil) let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil)
|> deliverOnMainQueue).startStandalone(next: { result in |> deliverOnMainQueue).startStandalone(next: { result in
@ -2940,9 +3009,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}) })
} }
public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController {
let controller = ShareController(context: context, subject: subject, externalShare: forceExternal) let controller = ShareController(context: context, subject: subject, externalShare: forceExternal)
controller.shareStory = shareStory controller.shareStory = shareStory
controller.enqueued = enqueued
controller.actionCompleted = actionCompleted controller.actionCompleted = actionCompleted
return controller return controller
} }