Stars ref

This commit is contained in:
Isaac 2024-11-29 20:20:26 +04:00
parent 2a424340a3
commit 835ea7358b
9 changed files with 259 additions and 20 deletions

View File

@ -1104,7 +1104,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
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?

View File

@ -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?

View File

@ -0,0 +1,54 @@
import Foundation
import UIKit
public final class TransformContents<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let fixedSize: CGSize?
private let translation: CGPoint
public init(content: AnyComponent<ChildEnvironment>, fixedSize: CGSize? = nil, translation: CGPoint) {
self.content = content
self.fixedSize = fixedSize
self.translation = translation
}
public static func ==(lhs: TransformContents<ChildEnvironment>, rhs: TransformContents<ChildEnvironment>) -> 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
}
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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,

View File

@ -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<Empty>?
private let sourceAvatar = ComponentView<Empty>()
private let sourceAvatarBadge = ComponentView<Empty>()
private let targetAvatar = ComponentView<Empty>()
private let targetAvatarBadge = ComponentView<Empty>()
private let sourceTargetArrow = UIImageView()
@ -96,6 +100,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var dailyRevenueText: ComponentView<Empty>?
private let titleTransformContainer: UIView
private let bottomPanelContainer: UIView
private let actionButton = ComponentView<Empty>()
@ -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<Empty>
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<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.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)

View File

@ -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
))
})

View File

@ -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)
}
}