diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 69735af767..da969afdc5 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1104,7 +1104,7 @@ public protocol SharedAccountContext: AnyObject { func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController - func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, mode: JoinAffiliateProgramScreenMode) -> ViewController + func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index e43f64c8b7..08021c7680 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -299,7 +299,7 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P if let cutoutRect { context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: cutoutRect) + context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height)) } }) let unroundedImage: UIImage? diff --git a/submodules/ComponentFlow/Source/Components/TransformContents.swift b/submodules/ComponentFlow/Source/Components/TransformContents.swift new file mode 100644 index 0000000000..6a00ea6586 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/TransformContents.swift @@ -0,0 +1,54 @@ +import Foundation +import UIKit + +public final class TransformContents: CombinedComponent { + public typealias EnvironmentType = ChildEnvironment + + private let content: AnyComponent + private let fixedSize: CGSize? + private let translation: CGPoint + + public init(content: AnyComponent, fixedSize: CGSize? = nil, translation: CGPoint) { + self.content = content + self.fixedSize = fixedSize + self.translation = translation + } + + public static func ==(lhs: TransformContents, rhs: TransformContents) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.fixedSize != rhs.fixedSize { + return false + } + if lhs.translation != rhs.translation { + return false + } + return true + } + + public static var body: Body { + let child = Child(environment: ChildEnvironment.self) + + return { context in + let child = child.update( + component: context.component.content, + environment: { context.environment[ChildEnvironment.self] }, + availableSize: context.availableSize, + transition: context.transition + ) + + let size = context.component.fixedSize ?? child.size + + var childFrame = child.size.centered(in: CGRect(origin: CGPoint(), size: size)) + childFrame.origin.x += context.component.translation.x + childFrame.origin.y += context.component.translation.y + + context.add(child + .position(childFrame.center) + ) + + return size + } + } +} diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index ce88413420..8008042739 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -2529,7 +2529,16 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { textColor = color } } - if let textColor { + if image.renderingMode == .alwaysOriginal { + let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - image.size.width * 0.5, y: attachment.frame.midY - image.size.height * 0.5 + 1.0), size: image.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(image.cgImage!, in: imageRect) + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + } else if let textColor { if let tintedImage = generateTintedImage(image: image, color: textColor) { let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) context.translateBy(x: imageRect.midX, y: imageRect.midY) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 9f106c9549..0df17ee073 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -798,7 +798,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var useInlineHLS = true if let data = context.currentAppConfiguration.with({ $0 }).data { - if let value = data["ios_inline_hls"] as? Double { + if let value = data["ios_inline_hls_v2"] as? Double { useInlineHLS = value != 0.0 } } diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 4455bed02e..d604e63e32 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -368,6 +368,7 @@ If you end your affiliate program: sourcePeer: bot.peer, commissionPermille: bot.commissionPermille, programDuration: bot.durationMonths, + revenuePerUser: bot.participants == 0 ? 0.0 : Double(bot.revenue) / Double(bot.participants), mode: .active(JoinAffiliateProgramScreenMode.Active( targetPeer: targetPeer, bot: bot, @@ -1422,6 +1423,7 @@ If you end your affiliate program: sourcePeer: botPeer, commissionPermille: item.program.commissionPermille, programDuration: item.program.durationMonths, + revenuePerUser: item.program.dailyRevenuePerUser?.totalValue ?? 0.0, mode: .join(JoinAffiliateProgramScreenMode.Join( initialTargetPeer: targetPeer, canSelectTargetPeer: false, diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 92c060735f..59a27e11f1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -28,6 +28,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { let sourcePeer: EnginePeer let commissionPermille: Int32 let programDuration: Int32? + let revenuePerUser: Double let mode: JoinAffiliateProgramScreen.Mode init( @@ -35,12 +36,14 @@ private final class JoinAffiliateProgramScreenComponent: Component { sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, + revenuePerUser: Double, mode: JoinAffiliateProgramScreen.Mode ) { self.context = context self.sourcePeer = sourcePeer self.commissionPermille = commissionPermille self.programDuration = programDuration + self.revenuePerUser = revenuePerUser self.mode = mode } @@ -86,6 +89,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { private var toast: ComponentView? private let sourceAvatar = ComponentView() + private let sourceAvatarBadge = ComponentView() private let targetAvatar = ComponentView() private let targetAvatarBadge = ComponentView() private let sourceTargetArrow = UIImageView() @@ -96,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { private let title = ComponentView() private let subtitle = ComponentView() + private var dailyRevenueText: ComponentView? private let titleTransformContainer: UIView private let bottomPanelContainer: UIView private let actionButton = ComponentView() @@ -128,6 +133,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { private var isChangingTargetPeer: Bool = false private var cachedCloseImage: UIImage? + private var inlineTextStarImage: UIImage? override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -576,25 +582,71 @@ private final class JoinAffiliateProgramScreenComponent: Component { transition.setFrame(view: targetAvatarView, frame: targetAvatarFrame) } - let badgeIconInset: CGFloat = 2.0 + if component.revenuePerUser != 0.0 { + var revenueString = String(format: "%.1f", component.revenuePerUser) + if revenueString.hasSuffix(".0") { + revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)]) + } + let sourceAvatarBadgeSize = self.sourceAvatarBadge.update( + transition: transition, + component: AnyComponent(BorderedBadgeComponent( + backgroundColor: environment.theme.list.itemDisclosureActions.constructive.fillColor, + cutoutColor: environment.theme.list.plainBackgroundColor, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Premium/PremiumStar", + tintColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor, + scaleFactor: 0.58 + )), + fixedSize: CGSize(width: 13.0, height: 10.0), + translation: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel) + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: revenueString, font: Font.regular(13.0), textColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor)) + ))) + ], spacing: 2.0)), + insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0), + cutoutWidth: 1.0 + UIScreenPixel) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let sourceAvatarBadgeFrame = CGRect(origin: CGPoint(x: sourceAvatarFrame.minX + floor((sourceAvatarFrame.width - sourceAvatarBadgeSize.width) * 0.5), y: sourceAvatarFrame.maxY - 7.0 - floor(sourceAvatarBadgeSize.height * 0.5)), size: sourceAvatarBadgeSize) + if let sourceAvatarBadgeView = self.sourceAvatarBadge.view { + if sourceAvatarBadgeView.superview == nil { + self.scrollContentView.addSubview(sourceAvatarBadgeView) + } + transition.setFrame(view: sourceAvatarBadgeView, frame: sourceAvatarBadgeFrame) + } + } + let targetAvatarBadgeSize = self.targetAvatarBadge.update( transition: transition, component: AnyComponent(BorderedBadgeComponent( - backgroundColor: UIColor(rgb: 0x8A7AFF), + backgroundColor: environment.theme.list.itemCheckColors.fillColor, cutoutColor: environment.theme.list.plainBackgroundColor, - content: AnyComponent(BundleIconComponent( - name: "Premium/PremiumStar", - tintColor: .white, - scaleFactor: 0.95 - )), - insets: UIEdgeInsets(top: badgeIconInset, left: badgeIconInset, bottom: badgeIconInset, right: badgeIconInset), - aspect: 1.0, - cutoutWidth: 1.0 + UIScreenPixel - )), + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Media Editor/Link", + tintColor: environment.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 0.75 + )), + translation: CGPoint(x: 0.0, y: 0.0) + )) + ), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(component.commissionPermille / 10)%", font: Font.regular(13.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)) + ))) + ], spacing: 2.0)), + insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0), + cutoutWidth: 1.0 + UIScreenPixel) + ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let targetAvatarBadgeFrame = CGRect(origin: CGPoint(x: targetAvatarFrame.maxX + 3.0 - targetAvatarBadgeSize.width, y: targetAvatarFrame.maxY + 3.0 - targetAvatarBadgeSize.height), size: targetAvatarBadgeSize) + let targetAvatarBadgeFrame = CGRect(origin: CGPoint(x: targetAvatarFrame.minX + floor((targetAvatarFrame.width - targetAvatarBadgeSize.width) * 0.5), y: targetAvatarFrame.maxY - 7.0 - floor(targetAvatarBadgeSize.height * 0.5)), size: targetAvatarBadgeSize) if let targetAvatarBadgeView = self.targetAvatarBadge.view { if targetAvatarBadgeView.superview == nil { self.scrollContentView.addSubview(targetAvatarBadgeView) @@ -723,12 +775,22 @@ private final class JoinAffiliateProgramScreenComponent: Component { } let titleString: String - let subtitleString: String + var subtitleString: String + var dailyRevenueString: String? let termsString: String switch currentMode { case .join: titleString = "Affiliate Program" subtitleString = "**\(component.sourcePeer.compactDisplayTitle)** will share **\(commissionTitle)** of the revenue from each user you refer to it for **\(durationTitle)**." + + if component.revenuePerUser != 0.0 { + var revenueString = String(format: "%.1f", component.revenuePerUser) + if revenueString.hasSuffix(".0") { + revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)]) + } + dailyRevenueString = "Daily revenue per user: #**\(revenueString)**" + } + termsString = "By joining this program, you afree to the [terms and conditions](https://telegram.org/terms) of Affiliate Programs." case let .active(active): titleString = "Referral Link" @@ -799,7 +861,115 @@ private final class JoinAffiliateProgramScreenComponent: Component { transition.setPosition(view: subtitleView, position: subtitleFrame.center) subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) } - contentHeight += subtitleSize.height + 23.0 + contentHeight += subtitleSize.height + + if let dailyRevenueString { + let dailyRevenueText: ComponentView + if let current = self.dailyRevenueText { + dailyRevenueText = current + } else { + dailyRevenueText = ComponentView() + self.dailyRevenueText = dailyRevenueText + } + + var inlineTextStarImage: UIImage? + if let current = self.inlineTextStarImage { + inlineTextStarImage = current + } else { + if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") { + let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top)) + })?.withRenderingMode(.alwaysOriginal) + self.inlineTextStarImage = inlineTextStarImage + } + } + + let attributedDailyRevenueString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(dailyRevenueString, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center)) + if let range = attributedDailyRevenueString.string.range(of: "#"), let starImage = inlineTextStarImage { + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + + let runDelegateData = RunDelegateData( + ascent: Font.regular(15.0).ascender, + descent: Font.regular(15.0).descender, + width: starImage.size.width + 2.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + attributedDailyRevenueString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: attributedDailyRevenueString.string)) + } + attributedDailyRevenueString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedDailyRevenueString.string)) + attributedDailyRevenueString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: attributedDailyRevenueString.string)) + attributedDailyRevenueString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedDailyRevenueString.string)) + } + + let dailyRevenueTextSize = dailyRevenueText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(attributedDailyRevenueString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + contentHeight += 16.0 + let dailyRevenueTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dailyRevenueTextSize.width) * 0.5), y: contentHeight), size: dailyRevenueTextSize) + if let dailyRevenueTextView = dailyRevenueText.view { + if dailyRevenueTextView.superview == nil { + self.scrollContentView.addSubview(dailyRevenueTextView) + } + transition.setPosition(view: dailyRevenueTextView, position: dailyRevenueTextFrame.center) + dailyRevenueTextView.bounds = CGRect(origin: CGPoint(), size: dailyRevenueTextFrame.size) + } + contentHeight += dailyRevenueTextSize.height + } else if let dailyRevenueText = self.dailyRevenueText { + self.dailyRevenueText = nil + dailyRevenueText.view?.removeFromSuperview() + } + + contentHeight += 23.0 var displayTargetPeer = false var isTargetPeerSelectable = false @@ -1116,6 +1286,7 @@ public class JoinAffiliateProgramScreen: ViewControllerComponentContainer { sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, + revenuePerUser: Double, mode: Mode ) { self.context = context @@ -1125,6 +1296,7 @@ public class JoinAffiliateProgramScreen: ViewControllerComponentContainer { sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, + revenuePerUser: revenuePerUser, mode: mode ), navigationBarAppearance: .none) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index a028ed60c1..e52670617e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -8685,6 +8685,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro sourcePeer: bot.peer, commissionPermille: bot.commissionPermille, programDuration: bot.durationMonths, + revenuePerUser: bot.participants == 0 ? 0.0 : Double(bot.revenue) / Double(bot.participants), mode: .active(JoinAffiliateProgramScreenMode.Active( targetPeer: targetPeer, bot: bot, @@ -8707,6 +8708,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro sourcePeer: .user(peer), commissionPermille: starRefProgram.commissionPermille, programDuration: starRefProgram.durationMonths, + revenuePerUser: starRefProgram.dailyRevenuePerUser?.totalValue ?? 0.0, mode: mode )) }) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index cbdc0d54f8..e43cf24cba 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2840,8 +2840,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return AffiliateProgramSetupScreen(context: context, initialContent: initialData) } - public func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, mode: JoinAffiliateProgramScreenMode) -> ViewController { - return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, mode: mode) + public func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController { + return JoinAffiliateProgramScreen(context: context, sourcePeer: sourcePeer, commissionPermille: commissionPermille, programDuration: programDuration, revenuePerUser: revenuePerUser, mode: mode) } }