Paid media improvements

This commit is contained in:
Ilya Laktyushin
2024-06-22 13:40:06 +04:00
parent 3b1d57f3cc
commit 7eee1436e7
7 changed files with 238 additions and 40 deletions

View File

@@ -43,6 +43,7 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/SheetComponent", "//submodules/Components/SheetComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramNotices", "//submodules/TelegramNotices",

View File

@@ -47,6 +47,7 @@ private final class ChannelStatsControllerArguments {
let requestTonWithdraw: () -> Void let requestTonWithdraw: () -> Void
let requestStarsWithdraw: () -> Void let requestStarsWithdraw: () -> Void
let showTimeoutTooltip: (Int32) -> Void
let openMonetizationIntro: () -> Void let openMonetizationIntro: () -> Void
let openMonetizationInfo: () -> Void let openMonetizationInfo: () -> Void
let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void let openTonTransaction: (RevenueStatsTransactionsContext.State.Transaction) -> Void
@@ -56,7 +57,7 @@ private final class ChannelStatsControllerArguments {
let presentCpmLocked: () -> Void let presentCpmLocked: () -> Void
let dismissInput: () -> Void let dismissInput: () -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateStarsSelected: @escaping (Bool) -> Void, requestTonWithdraw: @escaping () -> Void, requestStarsWithdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, openMonetizationIntro: @escaping () -> Void, openMonetizationInfo: @escaping () -> Void, openTonTransaction: @escaping (RevenueStatsTransactionsContext.State.Transaction) -> Void, openStarsTransaction: @escaping (StarsContext.State.Transaction) -> Void, expandTransactions: @escaping () -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openPostStats = openPostStats self.openPostStats = openPostStats
@@ -72,6 +73,7 @@ private final class ChannelStatsControllerArguments {
self.updateStarsSelected = updateStarsSelected self.updateStarsSelected = updateStarsSelected
self.requestTonWithdraw = requestTonWithdraw self.requestTonWithdraw = requestTonWithdraw
self.requestStarsWithdraw = requestStarsWithdraw self.requestStarsWithdraw = requestStarsWithdraw
self.showTimeoutTooltip = showTimeoutTooltip
self.openMonetizationIntro = openMonetizationIntro self.openMonetizationIntro = openMonetizationIntro
self.openMonetizationInfo = openMonetizationInfo self.openMonetizationInfo = openMonetizationInfo
self.openTonTransaction = openTonTransaction self.openTonTransaction = openTonTransaction
@@ -241,7 +243,7 @@ private enum StatsEntry: ItemListNodeEntry {
case adsTonBalanceInfo(PresentationTheme, String) case adsTonBalanceInfo(PresentationTheme, String)
case adsStarsBalanceTitle(PresentationTheme, String) case adsStarsBalanceTitle(PresentationTheme, String)
case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool) case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Int32?)
case adsStarsBalanceInfo(PresentationTheme, String) case adsStarsBalanceInfo(PresentationTheme, String)
case adsTransactionsTitle(PresentationTheme, String) case adsTransactionsTitle(PresentationTheme, String)
@@ -805,8 +807,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled): case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsCooldownUntilTimestamp):
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled { if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp {
return true return true
} else { } else {
return false return false
@@ -1050,9 +1052,11 @@ private enum StatsEntry: ItemListNodeEntry {
stats: stats, stats: stats,
canWithdraw: canWithdraw, canWithdraw: canWithdraw,
isEnabled: isEnabled, isEnabled: isEnabled,
actionCooldownUntilTimestamp: nil,
withdrawAction: { withdrawAction: {
arguments.requestTonWithdraw() arguments.requestTonWithdraw()
}, },
buyAdsAction: nil,
sectionId: self.section, sectionId: self.section,
style: .blocks style: .blocks
) )
@@ -1060,16 +1064,30 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openMonetizationInfo() arguments.openMonetizationInfo()
}) })
case let .adsStarsBalance(_, stats, canWithdraw, isEnabled): case let .adsStarsBalance(_, stats, canWithdraw, isEnabled, cooldownUntilTimestamp):
return MonetizationBalanceItem( return MonetizationBalanceItem(
context: arguments.context, context: arguments.context,
presentationData: presentationData, presentationData: presentationData,
stats: stats, stats: stats,
canWithdraw: canWithdraw, canWithdraw: canWithdraw,
isEnabled: isEnabled, isEnabled: isEnabled,
actionCooldownUntilTimestamp: cooldownUntilTimestamp,
withdrawAction: { withdrawAction: {
arguments.requestStarsWithdraw() var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
if remainingCooldownSeconds > 0 {
arguments.showTimeoutTooltip(cooldownUntilTimestamp)
} else {
arguments.requestStarsWithdraw()
}
} else {
arguments.requestStarsWithdraw()
}
}, },
buyAdsAction: nil,
sectionId: self.section, sectionId: self.section,
style: .blocks style: .blocks
) )
@@ -1538,7 +1556,7 @@ private func monetizationEntries(
if let starsData, starsData.balances.overallRevenue > 0 { if let starsData, starsData.balances.overallRevenue > 0 {
entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle)) entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle))
entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled)) entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > 0, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp))
entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo)) entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo))
} }
@@ -1780,6 +1798,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)? var openStarsTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
var requestTonWithdrawImpl: (() -> Void)? var requestTonWithdrawImpl: (() -> Void)?
var requestStarsWithdrawImpl: (() -> Void)? var requestStarsWithdrawImpl: (() -> Void)?
var showTimeoutTooltipImpl: ((Int32) -> Void)?
var updateStatusBarImpl: ((StatusBarStyle) -> Void)? var updateStatusBarImpl: ((StatusBarStyle) -> Void)?
var dismissInputImpl: (() -> Void)? var dismissInputImpl: (() -> Void)?
@@ -1911,6 +1930,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
requestStarsWithdraw: { requestStarsWithdraw: {
requestStarsWithdrawImpl?() requestStarsWithdrawImpl?()
}, },
showTimeoutTooltip: { timestamp in
showTimeoutTooltipImpl?(timestamp)
},
openMonetizationIntro: { openMonetizationIntro: {
let controller = MonetizationIntroScreen(context: context, openMore: {}) let controller = MonetizationIntroScreen(context: context, openMore: {})
pushImpl?(controller) pushImpl?(controller)
@@ -2374,6 +2396,52 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
} }
})) }))
} }
var tooltipScreen: UndoOverlayController?
var timer: Foundation.Timer?
showTimeoutTooltipImpl = { cooldownUntilTimestamp in
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let content: UndoOverlayContent = .universal(
animation: "anim_clock",
scale: 0.058,
colors: [:],
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
customUndoText: nil,
timeout: nil
)
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in
return true
})
tooltipScreen = controller
presentImpl?(controller)
if remainingCooldownSeconds < 3600 {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
if let tooltipScreen {
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
let content: UndoOverlayContent = .universal(
animation: "anim_clock",
scale: 0.058,
colors: [:],
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
customUndoText: nil,
timeout: nil
)
tooltipScreen.content = content
} else {
if let currentTimer = timer {
timer = nil
currentTimer.invalidate()
}
}
})
}
}
}
openTonTransactionImpl = { transaction in openTonTransactionImpl = { transaction in
let _ = (peer.get() let _ = (peer.get()
|> take(1) |> take(1)

View File

@@ -9,6 +9,9 @@ import ItemListUI
import SolidRoundedButtonNode import SolidRoundedButtonNode
import TelegramCore import TelegramCore
import TextFormat import TextFormat
import ComponentFlow
import ButtonComponent
import BundleIconComponent
final class MonetizationBalanceItem: ListViewItem, ItemListItem { final class MonetizationBalanceItem: ListViewItem, ItemListItem {
let context: AccountContext let context: AccountContext
@@ -16,7 +19,9 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
let stats: Stats let stats: Stats
let canWithdraw: Bool let canWithdraw: Bool
let isEnabled: Bool let isEnabled: Bool
let actionCooldownUntilTimestamp: Int32?
let withdrawAction: () -> Void let withdrawAction: () -> Void
let buyAdsAction: (() -> Void)?
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
@@ -26,7 +31,9 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
stats: Stats, stats: Stats,
canWithdraw: Bool, canWithdraw: Bool,
isEnabled: Bool, isEnabled: Bool,
actionCooldownUntilTimestamp: Int32?,
withdrawAction: @escaping () -> Void, withdrawAction: @escaping () -> Void,
buyAdsAction: (() -> Void)?,
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
style: ItemListStyle style: ItemListStyle
) { ) {
@@ -35,7 +42,9 @@ final class MonetizationBalanceItem: ListViewItem, ItemListItem {
self.stats = stats self.stats = stats
self.canWithdraw = canWithdraw self.canWithdraw = canWithdraw
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.withdrawAction = withdrawAction self.withdrawAction = withdrawAction
self.buyAdsAction = buyAdsAction
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
} }
@@ -85,12 +94,14 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let balanceTextNode: TextNode private let balanceTextNode: TextNode
private let valueTextNode: TextNode private let valueTextNode: TextNode
private var button = ComponentView<Empty>()
private var withdrawButtonNode: SolidRoundedButtonNode?
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
private var timer: Foundation.Timer?
private var item: MonetizationBalanceItem? private var item: MonetizationBalanceItem?
private var buttonLayout: (isStars: Bool, origin: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat)?
override var canBeSelected: Bool { override var canBeSelected: Bool {
return false return false
@@ -303,37 +314,91 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size) strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size)
if item.canWithdraw { strongSelf.buttonLayout = (isStars: isStars, origin: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: params.width, leftInset: leftInset, rightInset: rightInset)
let withdrawButtonNode: SolidRoundedButtonNode strongSelf.updateButton()
if let currentWithdrawButtonNode = strongSelf.withdrawButtonNode {
withdrawButtonNode = currentWithdrawButtonNode
} else {
var buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
buttonTheme = buttonTheme.withUpdated(disabledBackgroundColor: buttonTheme.backgroundColor, disabledForegroundColor: buttonTheme.foregroundColor.withAlphaComponent(0.6))
withdrawButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: buttonHeight, cornerRadius: 11.0)
withdrawButtonNode.pressed = { [weak self] in
if let self, let item = self.item, item.isEnabled {
item.withdrawAction()
}
}
strongSelf.addSubnode(withdrawButtonNode)
strongSelf.withdrawButtonNode = withdrawButtonNode
}
withdrawButtonNode.title = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw
withdrawButtonNode.isEnabled = item.isEnabled
let buttonWidth = contentSize.width - leftInset - rightInset
let _ = withdrawButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
withdrawButtonNode.frame = CGRect(x: leftInset, y: strongSelf.valueTextNode.frame.maxY + buttonSpacing + 3.0, width: buttonWidth, height: buttonHeight)
} else {
strongSelf.withdrawButtonNode?.removeFromSupernode()
strongSelf.withdrawButtonNode = nil
}
} }
}) })
} }
} }
func updateButton() {
guard let item = self.item, let (isStars, origin, width, leftInset, rightInset) = self.buttonLayout else {
return
}
if item.canWithdraw {
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = item.actionCooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
}
if remainingCooldownSeconds > 0 {
if self.timer == nil {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.updateButton()
})
}
} else {
if let timer = self.timer {
self.timer = nil
timer.invalidate()
}
}
let actionTitle = isStars ? item.presentationData.strings.Monetization_BalanceStarsWithdraw : item.presentationData.strings.Monetization_BalanceWithdraw
let content: AnyComponentWithIdentity<Empty>
if remainingCooldownSeconds > 0 {
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: item.presentationData.theme.list.itemCheckColors.fillColor.mixedWith(item.presentationData.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
], spacing: 3.0)))
], spacing: 1.0)
))
} else {
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)))
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: item.presentationData.theme.list.itemCheckColors.fillColor,
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: content,
isEnabled: item.isEnabled,
allowActionWhenDisabled: false,
displaysProgress: false,
action: { [weak self] in
guard let self, let item = self.item, item.isEnabled else {
return
}
item.withdrawAction()
}
)),
environment: {},
containerSize: CGSize(width: width - leftInset - rightInset, height: 50.0)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.view.addSubview(buttonView)
}
let buttonFrame = CGRect(origin: CGPoint(x: leftInset, y: origin), size: buttonSize)
buttonView.frame = buttonFrame
}
} else if let buttonView = self.button.view {
buttonView.removeFromSuperview()
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
} }
@@ -346,3 +411,16 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
} }
} }
func stringForRemainingTime(_ duration: Int32) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d", hours, minutes)
} else {
durationString = String(format: "%02d:%02d", minutes, seconds)
}
return durationString
}

View File

@@ -316,7 +316,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
theme: item.presentationData.theme, theme: item.presentationData.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: item.transaction.photo, media: item.transaction.media))), false), leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false),
icon: nil, icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in action: { [weak self] _ in

View File

@@ -18,13 +18,15 @@ public final class StarsAvatarComponent: Component {
let peer: StarsContext.State.Transaction.Peer let peer: StarsContext.State.Transaction.Peer
let photo: TelegramMediaWebFile? let photo: TelegramMediaWebFile?
let media: [Media] let media: [Media]
let backgroundColor: UIColor
public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media]) { public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer, photo: TelegramMediaWebFile?, media: [Media], backgroundColor: UIColor) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.peer = peer self.peer = peer
self.photo = photo self.photo = photo
self.media = media self.media = media
self.backgroundColor = backgroundColor
} }
public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool { public static func ==(lhs: StarsAvatarComponent, rhs: StarsAvatarComponent) -> Bool {
@@ -43,6 +45,9 @@ public final class StarsAvatarComponent: Component {
if !areMediaArraysEqual(lhs.media, rhs.media) { if !areMediaArraysEqual(lhs.media, rhs.media) {
return false return false
} }
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
return true return true
} }
@@ -51,6 +56,8 @@ public final class StarsAvatarComponent: Component {
private let backgroundView = UIImageView() private let backgroundView = UIImageView()
private let iconView = UIImageView() private let iconView = UIImageView()
private var imageNode: TransformImageNode? private var imageNode: TransformImageNode?
private var imageFrameNode: UIView?
private var secondImageNode: TransformImageNode?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
@@ -113,9 +120,48 @@ public final class StarsAvatarComponent: Component {
} }
} }
imageNode.frame = CGRect(origin: .zero, size: size) var imageFrame = CGRect(origin: .zero, size: size)
if component.media.count > 1 {
imageFrame = imageFrame.insetBy(dx: 2.0, dy: 2.0).offsetBy(dx: -2.0, dy: 2.0)
}
imageNode.frame = imageFrame
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
if component.media.count > 1 {
let secondImageNode: TransformImageNode
let imageFrameNode: UIView
if let current = self.secondImageNode, let currentFrame = self.imageFrameNode {
secondImageNode = current
imageFrameNode = currentFrame
} else {
secondImageNode = TransformImageNode()
secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.insertSubview(secondImageNode.view, belowSubview: imageNode.view)
self.secondImageNode = secondImageNode
imageFrameNode = UIView()
imageFrameNode.layer.cornerRadius = 8.0
self.insertSubview(imageFrameNode, belowSubview: imageNode.view)
self.imageFrameNode = imageFrameNode
if let image = component.media[1] as? TelegramMediaImage {
if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions {
dimensions = imageDimensions.cgSize.aspectFilled(size)
}
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
} else if let file = component.media[1] as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
dimensions = videoDimensions.cgSize.aspectFilled(size)
}
secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
}
}
imageFrameNode.backgroundColor = component.backgroundColor
secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: dimensions, boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
secondImageNode.frame = imageFrame.offsetBy(dx: 4.0, dy: -4.0)
imageFrameNode.frame = imageFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel)
}
self.backgroundView.isHidden = true self.backgroundView.isHidden = true
self.iconView.isHidden = true self.iconView.isHidden = true
self.avatarNode.isHidden = true self.avatarNode.isHidden = true

View File

@@ -458,6 +458,11 @@ public final class StarsImageComponent: Component {
dimensions = imageDimensions.cgSize.aspectFilled(imageSize) dimensions = imageDimensions.cgSize.aspectFilled(imageSize)
} }
secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false))
} else if let file = media[1] as? TelegramMediaFile {
if let videoDimensions = file.dimensions {
dimensions = videoDimensions.cgSize.aspectFilled(imageSize)
}
secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true))
} }
} }
imageFrameNode.backgroundColor = component.backgroundColor imageFrameNode.backgroundColor = component.backgroundColor

View File

@@ -299,7 +299,7 @@ final class StarsTransactionsListPanelComponent: Component {
theme: environment.theme, theme: environment.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media))), false), leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false),
icon: nil, icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in action: { [weak self] _ in