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";
"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, 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 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 makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController

View File

@ -7,6 +7,11 @@ import TelegramUIPreferences
import AnimationCache
import MultiAnimationRenderer
public enum StorySharingSubject {
case messages([Message])
case gift(StarGift.UniqueGift)
}
public protocol ShareControllerAccountContext: AnyObject {
var accountId: AccountRecordId { 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)
}
var hasCloseButton = false
if case .xmasPremiumGift = item.notice {
hasCloseButton = true
} 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 {
let hasCloseButton: Bool
switch item.notice {
case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto:
hasCloseButton = true
default:
hasCloseButton = false
}
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 {
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
interaction.openPeer(peer, .generic, nil, nil)
}, disabledAction: { _ in
if case let .peer(peer, _, _) = peer {
interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
}
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode 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)
}, disabledAction: { _ in
if case let .peer(peer, _, _) = peer {
interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic)
}
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue {
interaction.openStories(peerValue, sourceNode)
}
@ -581,15 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
case let .custom(showSelf, sections):
if !topPeers.isEmpty {
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
for (title, peerIds, hasActions) in sections {
var allSelected = true
@ -647,6 +631,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
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
if !sections.isEmpty, let selectionState {
var hasNonBirthdayPeers = false

View File

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

View File

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

View File

@ -664,6 +664,8 @@ public final class ShareController: ViewController {
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 {
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

View File

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

View File

@ -13,7 +13,6 @@ import TelegramPresentationData
import ShimmerEffect
import StickerPeekUI
import TextFormat
import Accelerate
final class StickerPackPreviewInteraction {
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,
forceExternal: false,
shareStory: nil,
enqueued: nil,
actionCompleted: { [weak parentNavigationController] in
if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View File

@ -8,6 +8,7 @@ import MediaResources
import Tuples
import ImageBlur
import FastBlur
import Accelerate
public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? {
if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in
@ -660,3 +661,100 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol
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 cachedPremiumGiftCodeOptions: Int8 = 42
public static let cachedProfileGifts: Int8 = 43
public static let recommendedBots: Int8 = 44
}
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> {
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> {
return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds)
}

View File

@ -357,6 +357,16 @@ public final class ButtonComponent: Component {
self.cornerRadius = cornerRadius
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

View File

@ -1847,6 +1847,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var transferGiftImpl: (() -> Void)?
var showAttributeInfoImpl: ((Any, Float) -> Void)?
var upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
var shareGiftImpl: (() -> Void)?
var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> 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
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
@ -2178,13 +2247,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let link = "https://t.me/nft/\(gift.slug)"
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Copy Link", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
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)
})))
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)
}, action: { [weak self] c, _ in
}, action: { c, _ in
c?.dismiss(completion: nil)
guard let self else {
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))
shareGiftImpl?()
})))
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)
}, action: { c, _ in
c?.dismiss(completion: nil)

View File

@ -88,6 +88,7 @@ public final class DrawingMessageRenderer {
private let isOverlay: Bool
private let isLink: Bool
private let isGift: Bool
private let wallpaperColor: UIColor?
private let messagesContainerNode: ASDisplayNode
private var avatarHeaderNode: ListViewItemHeaderNode?
@ -99,7 +100,8 @@ public final class DrawingMessageRenderer {
isNight: Bool = false,
isOverlay: Bool = false,
isLink: Bool = false,
isGift: Bool = false
isGift: Bool = false,
wallpaperColor: UIColor? = nil
) {
self.context = context
self.messages = messages
@ -107,6 +109,7 @@ public final class DrawingMessageRenderer {
self.isOverlay = isOverlay
self.isLink = isLink
self.isGift = isGift
self.wallpaperColor = wallpaperColor
self.messagesContainerNode = ASDisplayNode()
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)
}
})
}
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)
self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true)
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
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 {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
}
@ -351,14 +364,16 @@ public final class DrawingMessageRenderer {
messages: [Message],
parentView: UIView,
isLink: Bool = false,
isGift: Bool = false
isGift: Bool = false,
wallpaperDayColor: UIColor? = nil,
wallpaperNightColor: UIColor? = nil
) {
self.context = context
self.messages = messages
self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift)
self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift)
self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, 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, wallpaperColor: wallpaperNightColor)
self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift, wallpaperColor: nil)
parentView.addSubview(self.dayContainerNode.view)
parentView.addSubview(self.nightContainerNode.view)

View File

@ -308,7 +308,15 @@ public final class MediaEditor {
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 {
let duration: Double
@ -871,10 +879,13 @@ public final class MediaEditor {
let textureSource = UniversalTextureSource(renderTarget: renderTarget)
if case .message = self.self.subject {
switch self.subject {
case .message, .gift:
if let image = textureSourceResult.image {
self.wallpapers = (image, textureSourceResult.nightImage ?? image)
self.wallpapersValue = (image, textureSourceResult.nightImage ?? image)
}
default:
break
}
self.player = textureSourceResult.player
@ -1240,7 +1251,7 @@ public final class MediaEditor {
return values.withUpdatedNightTheme(nightTheme)
}
guard let (dayImage, nightImage) = self.wallpapers, let nightImage else {
guard let (dayImage, nightImage) = self.wallpapersValue, let nightImage else {
return
}

View File

@ -3388,53 +3388,83 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
messageFile = nil
}
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift)
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
let wallpaperColors: Signal<(UIColor?, UIColor?), NoError>
if let subject = self.subject, case .gift = subject {
wallpaperColors = self.mediaEditorPromise.get()
|> mapToSignal { mediaEditor in
if let mediaEditor {
return mediaEditor.wallpapers
|> filter {
$0 != nil
}
|> take(1)
|> map { result in
if let (dayImage, nightImage) = result {
return (getAverageColor(image: dayImage), nightImage.flatMap { getAverageColor(image: $0) })
}
return (nil, nil)
}
}
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:

View File

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

View File

@ -20,29 +20,29 @@ import Markdown
import SolidRoundedButtonNode
import PeerInfoPaneNode
private struct RecommendedChannelsListTransaction {
private struct RecommendedPeersListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let animated: Bool
}
private enum RecommendedChannelsListEntryStableId: Hashable {
private enum RecommendedPeersListEntryStableId: Hashable {
case addMember
case peer(PeerId)
}
private enum RecommendedChannelsListEntry: Comparable, Identifiable {
private enum RecommendedPeersListEntry: Comparable, Identifiable {
case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32)
var stableId: RecommendedChannelsListEntryStableId {
var stableId: RecommendedPeersListEntryStableId {
switch self {
case let .peer(_, _, peer, _):
return .peer(peer.id)
}
}
static func ==(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool {
static func ==(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool {
switch lhs {
case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers):
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 {
case let .peer(_, lhsIndex, _, _):
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 {
switch self {
case let .peer(_, _, peer, subscribers):
let subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers)
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: {
let text: ItemListPeerItemText
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)
}, setPeerIdWithRevealedOptions: { _, _ 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 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 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 chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
@ -98,9 +115,9 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
weak var parentController: ViewController?
private let listNode: ListView
private var currentEntries: [RecommendedChannelsListEntry] = []
private var currentState: (RecommendedChannels?, Bool)?
private var enqueuedTransactions: [RecommendedChannelsListTransaction] = []
private var currentEntries: [RecommendedPeersListEntry] = []
private var enqueuedTransactions: [RecommendedPeersListTransaction] = []
private var currentState: (RecommendedPeers?, Bool)?
private var unlockBackground: UIImageView?
private var unlockText: ComponentView<Empty>?
@ -145,32 +162,35 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.listNode.preloadPages = true
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.presentationDataPromise.get(),
context.engine.peers.recommendedChannels(peerId: peerId),
signal,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
return peer?.isPremium ?? false
}
)
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedChannels, isPremium in
guard let strongSelf = self else {
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedPeers, isPremium in
guard let self else {
return
}
strongSelf.currentState = (recommendedChannels, isPremium)
strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData)
self.currentState = (recommendedPeers, isPremium)
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
if let self {
self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring))
@ -215,8 +235,8 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout, let (recommendedChannels, isPremium) = self.currentState {
self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData)
if isFirstLayout, let (recommendedPeers, isPremium) = self.currentState {
self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData)
}
}
@ -225,8 +245,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
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) {
var entries: [RecommendedChannelsListEntry] = []
var entries: [RecommendedPeersListEntry] = []
if let channels = recommendedChannels?.channels {
for channel in channels {
@ -243,6 +271,42 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
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) {
@ -278,6 +342,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.view.addSubview(unlockBackground)
self.unlockBackground = unlockBackground
}
var isBots = false
if let (state, _) = self.currentState, state is RecommendedBots {
isBots = true
}
if let current = self.unlockButton {
unlockButton = current
@ -289,7 +358,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
unlockButton.animationLoopTime = 2.5
unlockButton.animation = "premium_unlock"
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
self?.unlockPressed()
@ -320,7 +389,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
transition: .immediate,
component: AnyComponent(
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,
maximumNumberOfLines: 0,
lineSpacing: 0.2

View File

@ -1008,6 +1008,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
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 profileGiftsContext: ProfileGiftsContext?
if case .user = kind {
@ -1309,6 +1316,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
status,
hasStories,
hasStoryArchive,
recommendedBots,
accountIsPremium,
savedMessagesPeer,
hasSavedMessagesChats,
@ -1322,7 +1330,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
premiumGiftOptions,
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
if isMyProfile {
availablePanes?.insert(.stories, at: 0)
@ -1373,6 +1381,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.botPreview, at: 0)
}
}
if let recommendedBots, recommendedBots.count > 0 {
availablePanes?.append(.similarBots)
}
} else {
availablePanes = nil
}
@ -1574,7 +1586,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.stories, at: 0)
}
if let recommendedChannels, !recommendedChannels.channels.isEmpty {
availablePanes?.append(.recommended)
availablePanes?.append(.similarChannels)
}
if case .peer = chatLocation {

View File

@ -545,8 +545,8 @@ private final class PeerInfoPendingPane {
} else {
preconditionFailure()
}
case .recommended:
paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction)
case .similarChannels, .similarBots:
paneNode = PeerInfoRecommendedPeersPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction)
case .savedMessagesChats:
paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController)
case .savedMessages:
@ -1201,8 +1201,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_PaneGroups
case .members:
title = presentationData.strings.PeerInfo_PaneMembers
case .recommended:
case .similarChannels:
title = presentationData.strings.PeerInfo_PaneRecommended
case .similarBots:
title = presentationData.strings.PeerInfo_PaneRecommendedBots
case .savedMessagesChats:
title = presentationData.strings.DialogList_TabTitle
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 {
self.storiesReady.set(false)
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
@ -12641,7 +12645,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
override public func loadDisplayNode() {
var initialPaneKey: PeerInfoPaneKey?
if self.switchToRecommendedChannels {
initialPaneKey = .recommended
initialPaneKey = .similarChannels
} else if self.switchToGifts {
initialPaneKey = .gifts
}

View File

@ -249,6 +249,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return .never()
}
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)

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
if let self, case let .starGiftUnique(gift, _, _, _, _, _, _) = action.action, case let .unique(uniqueGift) = gift {
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
}
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 {
switch entry {
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 visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
if visibleBusinessBotMessageIdValue < message.id {
@ -2836,22 +2830,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
visibleBusinessBotMessageId = message.id
}
}
if !hasAction {
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
case let .MessageGroupEntry(_, 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 visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
if visibleBusinessBotMessageIdValue < message.id {
@ -2861,13 +2847,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
visibleBusinessBotMessageId = message.id
}
}
if !hasAction {
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
}
default:

View File

@ -17,7 +17,7 @@ import AccountContext
private enum ChatReportPeerTitleButton: Equatable {
case block
case addContact(String?)
case addContact(String?, Bool)
case shareMyPhoneNumber
case reportSpam
case reportUserSpam
@ -30,11 +30,15 @@ private enum ChatReportPeerTitleButton: Equatable {
switch self {
case .block:
return strings.Conversation_BlockUser
case let .addContact(name):
case let .addContact(name, long):
if let name = name {
return strings.Conversation_AddNameToContacts(name).string
} else {
return strings.Conversation_AddToContacts
if long {
return strings.Conversation_AddToContactsLong
} else {
return strings.Conversation_AddToContacts
}
}
case .shareMyPhoneNumber:
return strings.Conversation_ShareMyPhoneNumber
@ -76,9 +80,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport
}
}
if buttons.isEmpty, let phone = peer.phone, !phone.isEmpty {
buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle))
buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle, buttons.isEmpty))
} else {
buttons.append(.addContact(nil))
buttons.append(.addContact(nil, buttons.isEmpty))
}
} else {
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)
}
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)?) {
let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil)
|> 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)
controller.shareStory = shareStory
controller.enqueued = enqueued
controller.actionCompleted = actionCompleted
return controller
}