mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Paid media improvements
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user