mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
2bf24b2bd9
commit
b0511f146e
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -20,7 +20,8 @@ public enum PeerInfoPaneKey: Int32 {
|
||||
case links
|
||||
case gifs
|
||||
case groupsInCommon
|
||||
case recommended
|
||||
case similarChannels
|
||||
case similarBots
|
||||
}
|
||||
|
||||
public struct PeerInfoStatusData: Equatable {
|
||||
|
@ -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
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user