Swiftgram/submodules/PremiumUI/Sources/GiveawayInfoController.swift
Ilya Laktyushin b4ca0637a6 Group boosts
2024-02-09 05:23:52 +04:00

543 lines
26 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
import TelegramPresentationData
import Markdown
import AlertUI
public func presentGiveawayInfoController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
messageId: EngineMessage.Id,
giveawayInfo: PremiumGiveawayInfo,
present: @escaping (ViewController) -> Void,
openLink: @escaping (String) -> Void
) {
var peerIds: [EnginePeer.Id] = [context.account.peerId]
if case let .ongoing(_, status) = giveawayInfo, case let .notAllowed(reason) = status, case let .channelAdmin(adminId) = reason {
peerIds.append(adminId)
}
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Messages.Message(id: messageId),
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
)
|> deliverOnMainQueue).startStandalone(next: { message, peerMap in
guard let message else {
return
}
let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway
let giveawayResults = message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults
var quantity: Int32 = 0
if let giveaway {
quantity = giveaway.quantity
} else if let giveawayResults {
quantity = giveawayResults.winnersCount + giveawayResults.unclaimedCount
}
var months: Int32 = 0
if let giveaway {
months = giveaway.months
} else if let giveawayResults {
months = giveawayResults.months
}
var prizeDescription: String?
if let giveaway {
prizeDescription = giveaway.prizeDescription
} else if let giveawayResults {
prizeDescription = giveawayResults.prizeDescription
}
var untilDateValue: Int32 = 0
if let giveaway {
untilDateValue = giveaway.untilDate
} else if let giveawayResults {
untilDateValue = giveawayResults.untilDate
}
var onlyNewSubscribers = false
if let giveaway, giveaway.flags.contains(.onlyNewSubscribers) {
onlyNewSubscribers = true
} else if let giveawayResults, giveawayResults.flags.contains(.onlyNewSubscribers) {
onlyNewSubscribers = true
}
let author = message.forwardInfo?.author ?? message.author?._asPeer()
var isGroup = false
if let channel = author as? TelegramChannel, case .group = channel.info {
isGroup = true
}
var groupsAndChannels = false
var channelsCount: Int32 = 1
if let giveaway {
channelsCount = Int32(giveaway.channelPeerIds.count)
var channelCount = 0
var groupCount = 0
for peerId in giveaway.channelPeerIds {
if let peer = message.peers[peerId] as? TelegramChannel {
switch peer.info {
case .broadcast:
channelCount += 1
case .group:
groupCount += 1
}
}
}
if groupCount > 0 && channelCount > 0 {
groupsAndChannels = true
}
} else if let giveawayResults {
channelsCount = 1 + giveawayResults.additionalChannelsCount
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var peerName = ""
if let channel = author as? TelegramChannel {
peerName = EnginePeer(channel).compactDisplayTitle
}
let timeZone = TimeZone.current
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)
let title: String
let text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
var additionalPrizes = ""
if let prizeDescription, !prizeDescription.isEmpty {
additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(quantity) \(prizeDescription)").string
}
switch giveawayInfo {
case let .ongoing(start, status):
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
title = presentationData.strings.Chat_Giveaway_Info_Title
let intro: String
if case .almostOver = status {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
}
} else {
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
}
}
var otherText: String = ""
if channelsCount > 1 {
if isGroup {
if groupsAndChannels {
if channelsCount == 2 {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroupsAndChannels(Int32(channelsCount - 1))
}
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroups(Int32(channelsCount - 1))
}
} else {
if groupsAndChannels {
if channelsCount == 2 {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherGroups(Int32(channelsCount - 1))
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannelsAndGroups(Int32(channelsCount - 1))
}
} else {
otherText = presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))
}
}
}
let ending: String
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, otherText, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = isGroup ? presentationData.strings.Chat_Giveaway_Info_Group_RandomMembers(quantity) : presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, otherText).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string
}
}
var participation: String
switch status {
case .notQualified:
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, otherText, untilDate).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string
}
case let .notAllowed(reason):
switch reason {
case let .joinedTooEarly(joinedOn):
let joinDate = stringForDate(timestamp: joinedOn, strings: presentationData.strings)
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedJoinedEarly(joinDate).string
case let .channelAdmin(adminId):
var channelName = peerName
var isGroup = false
if let maybePeer = peerMap[adminId], let peer = maybePeer {
channelName = peer.compactDisplayTitle
if case let .channel(channel) = peer, case .group = channel.info {
isGroup = true
}
}
participation = isGroup ? presentationData.strings.Chat_Giveaway_Info_NotAllowedAdminGroup(channelName).string : presentationData.strings.Chat_Giveaway_Info_NotAllowedAdmin(channelName).string
case .disallowedCountry:
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry
}
case .participating:
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, otherText).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string
}
case .almostOver:
participation = presentationData.strings.Chat_Giveaway_Info_AlmostOver
}
if !participation.isEmpty {
participation = "\n\n\(participation)"
}
text = "\(intro)\(additionalPrizes)\n\n\(ending)\(participation)"
case let .finished(status, start, finish, _, activatedCount):
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings)
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
let intro: String
if isGroup {
intro = presentationData.strings.Chat_Giveaway_Info_Group_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
}
var ending: String
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = isGroup ? presentationData.strings.Chat_Giveaway_Info_Group_RandomMembers(quantity) : presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string
}
}
if activatedCount > 0 {
ending += " " + presentationData.strings.Chat_Giveaway_Info_ActivatedLinks(activatedCount)
}
var result: String
switch status {
case .refunded:
result = ""
warning = presentationData.strings.Chat_Giveaway_Info_Refunded
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n"
case let .won(slug):
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
openLink(slug)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
}
text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)"
}
let alertController = giveawayInfoAlertController(
context: context,
updatedPresentationData: updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
present(alertController)
})
}
private final class GiveawayInfoAlertContentNode: AlertContentNode {
private let title: String
private let text: String
private let warning: String?
private let titleNode: ASTextNode
private let textNode: ASTextNode
fileprivate let warningBackgroundNode: ASImageNode
fileprivate let warningTextNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
public var theme: PresentationTheme
public override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) {
self.theme = ptheme
self.title = title
self.text = text
self.warning = warning
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.warningBackgroundNode = ASImageNode()
self.warningBackgroundNode.displaysAsynchronously = false
self.warningTextNode = ImmediateTextNode()
self.warningTextNode.maximumNumberOfLines = 0
self.warningTextNode.lineSpacing = 0.1
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.warningBackgroundNode)
self.addSubnode(self.warningTextNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
public override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
self.textNode.attributedText = attributedText
self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center)
self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
if "".isEmpty {
effectiveActionLayout = .vertical
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
var warningHeight: CGFloat = 0.0
if let _ = self.warning {
let warningSize = self.warningTextNode.updateLayout(measureSize)
let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize)
transition.updateFrame(node: self.warningTextNode, frame: warningFrame)
transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0))
warningHeight += warningSize.height + 26.0
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}