Various improvements

This commit is contained in:
Ilya Laktyushin 2026-01-31 07:08:06 +04:00
parent c509a6e1a2
commit cbe98a3fc6
15 changed files with 528 additions and 364 deletions

View file

@ -15672,7 +15672,7 @@ Error: %8$@";
"Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter";
"Notification.StarsGift.Crafted" = "You crafted a new unique collectible";
"Notification.StarsGift.Crafted" = "You crafted a new gift";
"Gift.Craft.Title" = "Craft Gift";
"Gift.Craft.Description" = "Add up to **4 gifts** to craft new\n**$ %@**.\n\nIf crafting fails, all used gifts\nwill be lost.";
@ -15757,3 +15757,11 @@ Error: %8$@";
"Notification.GroupCreatorChangeApplied" = "%1$@ has transferred ownership of the group to %2$@.";
"Notification.StarGift.Crafted.Title" = "Crafted Gift";
"Notification.StarGift.Upgraded.Title" = "Upgraded Gift";
"Notification.StarGift.Purchased.Title" = "Purchased Gift";
"LeaveGroup.Title" = "Leave %@";
"LeaveGroup.Text" = "If you leave, **%1$@** will become the owner of **%2$@** in **1 week**.";
"LeaveGroup.AppointAnotherOwner" = "Appoint Another Owner";
"LeaveGroup.Proceed" = "Leave Group";
"AppointAnotherOwner.Title" = "Appoint Another Owner";

View file

@ -51,17 +51,16 @@ extension ChatListControllerImpl {
)
)
))
//TODO:localize
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: "Leave \(peer.compactDisplayTitle)")
AlertTitleComponent(title: self.presentationData.strings.LeaveGroup_Title(peer.compactDisplayTitle).string)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain("If you leave, **\(nextCreator.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))** will become the owner of **\(peer.compactDisplayTitle)** in **1 week**."))
AlertTextComponent(content: .plain(self.presentationData.strings.LeaveGroup_Text(nextCreator.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), peer.compactDisplayTitle).string))
)
))
@ -70,16 +69,18 @@ extension ChatListControllerImpl {
configuration: .init(actionAlignment: .vertical),
content: content,
actions: [
.init(title: "Appoint Another Owner", action: { [weak self] in
.init(title: self.presentationData.strings.LeaveGroup_AppointAnotherOwner, action: { [weak self] in
guard let self else {
return
}
self.presentOwnershipTransfer(chatPeer: peer)
self.presentOwnershipTransfer(chatPeer: peer, leaveGroup: {
completion(true)
})
}),
.init(title: "Cancel", action: {
.init(title: self.presentationData.strings.Common_Cancel, action: {
completion(false)
}),
.init(title: "Leave Group", type: .destructive, action: {
.init(title: self.presentationData.strings.LeaveGroup_Proceed, type: .destructive, action: {
completion(true)
})
]
@ -90,7 +91,7 @@ extension ChatListControllerImpl {
}
}
func presentOwnershipTransfer(chatPeer: EnginePeer) {
func presentOwnershipTransfer(chatPeer: EnginePeer, leaveGroup: @escaping () -> Void) {
let presentController: (ViewController) -> Void = { [weak self] c in
if let topController = self?.navigationController?.topViewController as? ViewController {
topController.present(c, in: .window(.root))
@ -121,7 +122,9 @@ extension ChatListControllerImpl {
case .creator:
return
case let .member(_, _, adminInfo, _, _, _):
let _ = adminInfo
if adminInfo == nil {
let _ = self.context.engine.peers.updateChannelAdminRights(peerId: chatPeer.id, adminId: peer.id, rights: TelegramChatAdminRights(rights: .all), rank: nil).start()
}
let _ = (self.context.engine.peers.checkOwnershipTranfserAvailability(memberId: peer.id) |> deliverOnMainQueue).start(error: { [weak self] error in
guard let self, case let .user(user) = peer else {
@ -141,6 +144,8 @@ extension ChatListControllerImpl {
},
completion: { _ in
dismissController?()
leaveGroup()
}
)
presentController(controller)

View file

@ -53,8 +53,7 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe
let title: String
switch params.mode {
case .ownershipTransfer:
//TODO:localize
title = "Appoint Another Owner"
title = self.presentationData.strings.AppointAnotherOwner_Title
let titleView = CounterControllerTitleView(theme: self.presentationData.theme)
titleView.title = CounterControllerTitle(title: title, counter: " ")
self.navigationItem.titleView = titleView

View file

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 224
return 222
}
public func parseMessage(_ data: Data!) -> Any! {

View file

@ -65,6 +65,8 @@ public final class AlertTitleComponent: Component {
let environment = environment[AlertComponentEnvironment.self]
let inset: CGFloat = -6.0
let titleConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
@ -77,13 +79,13 @@ public final class AlertTitleComponent: Component {
maximumNumberOfLines: 0
)),
environment: {},
containerSize: availableSize
containerSize: titleConstrainedSize
)
let titleOriginX: CGFloat
switch component.alignment {
case .default:
titleOriginX = 0.0
titleOriginX = inset
case .center:
titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0)
}
@ -280,7 +282,8 @@ public final class AlertTextComponent: Component {
break
}
let textConstrainedSize = CGSize(width: availableSize.width, height: availableSize.height)
let inset: CGFloat = -6.0
let textConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let textSize = self.text.update(
transition: transition,
@ -314,7 +317,7 @@ public final class AlertTextComponent: Component {
containerSize: textConstrainedSize
)
var textOffset = CGPoint()
var textOffset = CGPoint(x: inset, y: 0.0)
if hasCenterAlignment {
textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0)
}

View file

@ -581,7 +581,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let isChannelGift = item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel || channelPeerId != nil
if isSelfGift {
title = item.presentationData.strings.Notification_StarGift_Self_Title
title = item.presentationData.strings.Notification_StarGift_Purchased_Title
} else {
if isPrepaidUpgrade && senderPeerId == channelPeerId {
title = item.presentationData.strings.Gift_View_Unknown_Title
@ -714,8 +714,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} else if isSelfGift {
if isCrafted {
title = item.presentationData.strings.Notification_StarGift_Crafted_Title
} else if isUpgrade {
title = item.presentationData.strings.Notification_StarGift_Upgraded_Title
} else {
title = item.presentationData.strings.Notification_StarGift_Self_Title
title = item.presentationData.strings.Notification_StarGift_Purchased_Title
}
} else if item.message.id.peerId.isTelegramNotifications {
title = item.presentationData.strings.Notification_StarGift_TitleShort

View file

@ -46,7 +46,6 @@ swift_library(
"//submodules/TooltipUI",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
"//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/ConfettiEffect",
"//submodules/TelegramNotices",
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView",

View file

@ -34,7 +34,7 @@ final class DialIndicatorComponent: Component {
value: Int,
suffix: String,
isVisible: Bool = true,
isFlipped: Bool = false,
isFlipped: Bool = false
) {
self.content = content
self.backgroundColor = backgroundColor
@ -224,3 +224,72 @@ final class DialIndicatorComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ColorSwatchComponent: Component {
let innerColor: UIColor
let outerColor: UIColor
public init(
innerColor: UIColor,
outerColor: UIColor
) {
self.innerColor = innerColor
self.outerColor = outerColor
}
public static func ==(lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool {
if lhs.innerColor != rhs.innerColor {
return false
}
if lhs.outerColor != rhs.outerColor {
return false
}
return true
}
public final class View: UIImageView {
private var component: ColorSwatchComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
if previousComponent?.innerColor != component.innerColor || previousComponent?.outerColor != component.outerColor {
self.image = generateImage(availableSize, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Premium/Craft/DialColorMask"), let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor] = [component.innerColor.cgColor, component.outerColor.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -31,13 +31,9 @@ import LottieComponent
import TooltipUI
import TextFormat
import GlassBackgroundComponent
import SpaceWarpView
import ConfettiEffect
import TelegramNotices
private let backdropButtonTag = GenericComponentViewTag()
private let symbolButtonTag = GenericComponentViewTag()
private final class CraftGiftPageContent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -145,9 +141,9 @@ private final class CraftGiftPageContent: Component {
private var craftTable = ComponentView<Empty>()
private var backdropDial = ComponentView<Empty>()
private var symbolDial = ComponentView<Empty>()
private var variantsButton = ComponentView<Empty>()
private var attributeDials: [AnyHashable: ComponentView<Empty>] = [:]
private var attributeDialTags: [AnyHashable: GenericComponentViewTag] = [:]
private var variantsButton: ComponentView<Empty>?
private var variantsButtonMeasure = ComponentView<Empty>()
private let craftingTitle = ComponentView<Empty>()
@ -461,282 +457,8 @@ private final class CraftGiftPageContent: Component {
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
craftContentHeight += 291.0
craftContentHeight += descriptionTextSize.height
craftContentHeight += 25.0
var attributes: [ResaleGiftsContext.Attribute: StarGift.UniqueGift.Attribute] = [:]
var backdropAttributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
var patternAttributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
for gift in selectedGifts.values {
for attribute in gift.gift.attributes {
switch attribute {
case let .backdrop(_, id, _, _, _, _, _):
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
attributes[attributeId] = attribute
if let count = backdropAttributeCount[attributeId] {
backdropAttributeCount[attributeId] = count + 1
} else {
backdropAttributeCount[attributeId] = 1
}
case let .pattern(_, file, _):
let attributeId: ResaleGiftsContext.Attribute = .pattern(file.fileId.id)
attributes[attributeId] = attribute
if let count = patternAttributeCount[attributeId] {
patternAttributeCount[attributeId] = count + 1
} else {
patternAttributeCount[attributeId] = 1
}
default:
break
}
}
}
func mostFrequentAttribute(from counts: [ResaleGiftsContext.Attribute: Int32]) -> (attribute: StarGift.UniqueGift.Attribute, count: Int32)? {
guard let (id, count) = counts.max(by: { $0.value < $1.value }),
count > 1,
let attribute = attributes[id] else {
return nil
}
return (attribute, count)
}
var possibleBackdrop: (StarGift.UniqueGift.Attribute, Int32)?
var possiblePattern: (StarGift.UniqueGift.Attribute, Int32)?
var backdropColor: UIColor = .white
var backdropPermille: Int = 0
var backdropName = ""
var symbolFile: TelegramMediaFile?
var symbolPermille: Int = 0
var symbolName = ""
for attribute in component.gift.attributes {
switch attribute {
case .backdrop:
possibleBackdrop = (attribute, 1)
case .pattern:
possiblePattern = (attribute, 1)
default:
break
}
}
if let betterBackdrop = mostFrequentAttribute(from: backdropAttributeCount) {
possibleBackdrop = betterBackdrop
}
if let betterPattern = mostFrequentAttribute(from: patternAttributeCount) {
possiblePattern = betterPattern
}
let appConfiguration = component.context.currentAppConfiguration.with { $0 }
let giftCraftConfiguration = GiftCraftConfiguration.with(appConfiguration: appConfiguration)
if case let .backdrop(name, _, innerColor, _, _, _, _) = possibleBackdrop?.0 {
backdropColor = UIColor(rgb: UInt32(bitPattern: innerColor))
backdropName = name
}
if let possibleBackdrop {
backdropPermille = Int(giftCraftConfiguration.craftAttributePermilles[Int(possibleBackdrop.1 - 1)])
}
if case let .pattern(name, file, _) = possiblePattern?.0 {
symbolFile = file
symbolName = name
}
if let possiblePattern {
symbolPermille = Int(giftCraftConfiguration.craftAttributePermilles[Int(possiblePattern.1 - 1)])
}
let backdropDialSize = self.backdropDial.update(
transition: transition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
DialIndicatorComponent(
content: AnyComponentWithIdentity(
id: "color",
component: AnyComponent(
BundleIconComponent(name: "Components/ColorMask", tintColor: backdropColor)
)
),
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 48.0,
lineWidth: 4.0,
fontSize: 10.0,
progress: CGFloat(backdropPermille) / 10.0 / 100.0,
value: backdropPermille / 10,
suffix: "%"
)
),
action: { [weak self] in
guard let self else {
return
}
#if DEBUG
self.component?.externalState.testFailOrSuccess = true
#endif
self.showAttributeInfo(tag: backdropButtonTag, text: environment.strings.Gift_Craft_BackdropTooltip("\(backdropPermille / 10)", backdropName).string)
},
tag: backdropButtonTag
)
),
environment: {},
containerSize: availableSize
)
let symbolDialSize = self.symbolDial.update(
transition: transition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
DialIndicatorComponent(
content: symbolFile.flatMap { AnyComponentWithIdentity(
id: "symbol",
component: AnyComponent(
LottieComponent(
content: LottieComponent.ResourceContent(
context: component.context,
file: $0,
attemptSynchronously: true,
providesPlaceholder: true
),
color: .white,
size: CGSize(width: 32.0, height: 32.0)
)
)
) } ?? AnyComponentWithIdentity(
id: "empty", component: AnyComponent(Rectangle(color: .clear))),
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 48.0,
contentSize: CGSize(width: 28.0, height: 28.0),
lineWidth: 4.0,
fontSize: 10.0,
progress: CGFloat(symbolPermille) / 10.0 / 100.0,
value: symbolPermille / 10,
suffix: "%"
)
),
action: { [weak self] in
guard let self else {
return
}
#if DEBUG
self.component?.externalState.testFailOrSuccess = false
#endif
self.showAttributeInfo(tag: symbolButtonTag, text: environment.strings.Gift_Craft_SymbolTooltip("\(symbolPermille / 10)", symbolName).string)
},
tag: symbolButtonTag
)
),
environment: {},
containerSize: availableSize
)
craftContentHeight += backdropDialSize.height
craftContentHeight += 15.0
let variantsString = environment.strings.Gift_Craft_ViewVariants
let variantsButtonMeasure = self.variantsButtonMeasure.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))),
environment: {},
containerSize: availableSize
)
let variantsButtonSize = CGSize(width: variantsButtonMeasure.width + 87.0, height: 24.0)
if let gift = self.starGiftsMap[component.gift.giftId] {
var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
if let upgradePreview = self.upgradePreview {
var i = 0
for attribute in upgradePreview {
if case .model = attribute {
switch i {
case 0:
variant1 = .preview(attributes: [attribute], rarity: nil)
case 1:
variant2 = .preview(attributes: [attribute], rarity: nil)
case 2:
variant3 = .preview(attributes: [attribute], rarity: nil)
default:
break
}
i += 1
}
}
}
let _ = self.variantsButton.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: variantsButtonSize,
backgroundColor: component.colors.3,
isDark: true,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "content", component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "icon1", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant1,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "icon2", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant2,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "icon3", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant3,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "text", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .white)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white)
))
], spacing: 3.0))),
action: { [weak self] _ in
self?.openUpgradeVariants()
}
)
),
environment: {},
containerSize: availableSize
)
}
craftContentHeight += 160.0
let originalCraftContentHeight = craftContentHeight
if !"".isEmpty, isCrafting {
craftContentHeight = component.screenSize.height
}
let descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionTextSize.width) * 0.5), y: craftContentHeight - 145.0 - 78.0 - 115.0), size: descriptionTextSize)
let descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionTextSize.width) * 0.5), y: craftContentHeight), size: descriptionTextSize)
if let descriptionTextView = self.descriptionText.view {
if descriptionTextView.superview == nil {
self.addSubview(descriptionTextView)
@ -745,52 +467,395 @@ private final class CraftGiftPageContent: Component {
transition.setAlpha(view: descriptionTextView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: descriptionTextView.layer, radius: isCrafting ? 10.0 : 0.0)
}
craftContentHeight += descriptionTextSize.height
craftContentHeight += 16.0
let backdropDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 - 9.0 - backdropDialSize.width, y: craftContentHeight - 145.0 - 78.0), size: backdropDialSize)
if let backdropDialView = self.backdropDial.view {
if backdropDialView.superview == nil {
self.addSubview(backdropDialView)
}
transition.setFrame(view: backdropDialView, frame: backdropDialFrame)
transition.setAlpha(view: backdropDialView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: backdropDialView.layer, radius: isCrafting ? 10.0 : 0.0)
}
let symbolDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 + 9.0, y: craftContentHeight - 145.0 - 78.0), size: symbolDialSize)
if let symbolDialView = self.symbolDial.view {
if symbolDialView.superview == nil {
self.addSubview(symbolDialView)
}
transition.setFrame(view: symbolDialView, frame: symbolDialFrame)
transition.setAlpha(view: symbolDialView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: symbolDialView.layer, radius: isCrafting ? 10.0 : 0.0)
}
let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight - 145.0), size: variantsButtonSize)
var varitantsButtonTransition = transition
if let variantsButtonView = self.variantsButton.view {
if variantsButtonView.superview == nil && component.displayState == .default {
varitantsButtonTransition = .immediate
if let symbolDialView = self.symbolDial.view {
self.insertSubview(variantsButtonView, aboveSubview: symbolDialView)
} else {
self.addSubview(variantsButtonView)
var attributes: [ResaleGiftsContext.Attribute: StarGift.UniqueGift.Attribute] = [:]
var backdropAttributeCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
var patternAttributeCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
for (index, gift) in selectedGifts {
for attribute in gift.gift.attributes {
switch attribute {
case let .backdrop(_, id, _, _, _, _, _):
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
attributes[attributeId] = attribute
if let (minPosition, count) = backdropAttributeCount[attributeId] {
backdropAttributeCount[attributeId] = (min(index, minPosition), count + 1)
} else {
backdropAttributeCount[attributeId] = (index, 1)
}
case let .pattern(_, file, _):
let attributeId: ResaleGiftsContext.Attribute = .pattern(file.fileId.id)
attributes[attributeId] = attribute
if let (minPosition, count) = patternAttributeCount[attributeId] {
patternAttributeCount[attributeId] = (min(index, minPosition), count + 1)
} else {
patternAttributeCount[attributeId] = (index, 1)
}
default:
break
}
}
varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame)
varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0)
if component.displayState == .crafting {
}
var attributesCount: [ResaleGiftsContext.Attribute: (Int32, Int)] = [:]
for (attributeId, value) in backdropAttributeCount {
attributesCount[attributeId] = value
}
for (attributeId, value) in patternAttributeCount {
attributesCount[attributeId] = value
}
var backdropAttributes: [(ResaleGiftsContext.Attribute, Int)] = []
for (attributeId, count) in backdropAttributeCount {
backdropAttributes.append((attributeId, count.1))
}
backdropAttributes = backdropAttributes.sorted(by: { lhs, rhs in
if lhs.1 != rhs.1 {
return lhs.1 > rhs.1
} else {
return attributesCount[lhs.0]!.0 < attributesCount[rhs.0]!.0
}
})
var patternAttributes: [(ResaleGiftsContext.Attribute, Int)] = []
for (attributeId, count) in patternAttributeCount {
patternAttributes.append((attributeId, count.1))
}
patternAttributes = patternAttributes.sorted(by: { lhs, rhs in
if lhs.1 != rhs.1 {
return lhs.1 > rhs.1
} else {
return attributesCount[lhs.0]!.0 < attributesCount[rhs.0]!.0
}
})
var combinedAttributes: [ResaleGiftsContext.Attribute] = []
for (attributeId, _) in backdropAttributes {
combinedAttributes.append(attributeId)
}
for (attributeId, _) in patternAttributes {
combinedAttributes.append(attributeId)
}
let appConfiguration = component.context.currentAppConfiguration.with { $0 }
let giftCraftConfiguration = GiftCraftConfiguration.with(appConfiguration: appConfiguration)
var firstRowCount = 0
var secondRowCount = 0
switch combinedAttributes.count {
case 0, 1, 2, 3, 4:
firstRowCount = combinedAttributes.count
case 5:
firstRowCount = 2
secondRowCount = 3
case 6:
firstRowCount = 3
secondRowCount = 3
case 7:
firstRowCount = 3
secondRowCount = 4
case 8:
firstRowCount = 4
secondRowCount = 4
default:
break
}
let attributeDialSpacing: CGFloat = 18.0
let attributeDialSize = CGSize(width: 48.0, height: 48.0)
let attributeFirstRowTotalWidth = CGFloat(firstRowCount) * attributeDialSize.width + CGFloat(firstRowCount - 1) * attributeDialSpacing
let attributeSecondRowTotalWidth = CGFloat(secondRowCount) * attributeDialSize.width + CGFloat(secondRowCount - 1) * attributeDialSpacing
var attributeDialFrame: CGRect = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - attributeFirstRowTotalWidth) / 2.0), y: craftContentHeight), size: attributeDialSize)
craftContentHeight += attributeDialSize.height
craftContentHeight += 39.0
var validIds: [AnyHashable] = []
var attributeDialIndex = 0
for attribute in combinedAttributes {
let itemId = AnyHashable(attribute)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.attributeDials[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.attributeDials[itemId] = visibleItem
itemTransition = .immediate
}
let tag: GenericComponentViewTag
if let current = self.attributeDialTags[itemId] {
tag = current
} else {
tag = GenericComponentViewTag()
self.attributeDialTags[itemId] = tag
}
let attributeCount = attributesCount[attribute]?.1 ?? 0
let permille = Int(giftCraftConfiguration.craftAttributePermilles[selectedGifts.count - 1][attributeCount - 1])
let dialContent: AnyComponentWithIdentity<Empty>
var dialContentSize: CGSize?
let tooltipText: String
switch attribute {
case .backdrop:
guard case let .backdrop(name, _, innerColor, outerColor, _, _, _) = attributes[attribute] else {
continue
}
dialContent = AnyComponentWithIdentity(
id: "color",
component: AnyComponent(
ColorSwatchComponent(
innerColor: UIColor(rgb: UInt32(bitPattern: innerColor)),
outerColor: UIColor(rgb: UInt32(bitPattern: outerColor))
)
)
)
tooltipText = environment.strings.Gift_Craft_BackdropTooltip("\(permille / 10)", name).string
case .pattern:
guard case let .pattern(name, file, _) = attributes[attribute] else {
continue
}
dialContent = AnyComponentWithIdentity(
id: "symbol",
component: AnyComponent(
LottieComponent(
content: LottieComponent.ResourceContent(
context: component.context,
file: file,
attemptSynchronously: true,
providesPlaceholder: true
),
color: .white,
size: CGSize(width: 32.0, height: 32.0)
)
)
)
dialContentSize = CGSize(width: 30.0, height: 30.0)
tooltipText = environment.strings.Gift_Craft_SymbolTooltip("\(permille / 10)", name).string
default:
continue
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
DialIndicatorComponent(
content: dialContent,
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 48.0,
contentSize: dialContentSize,
lineWidth: 4.0,
fontSize: 10.0,
progress: CGFloat(permille) / 10.0 / 100.0,
value: permille / 10,
suffix: "%"
)
),
action: { [weak self] in
guard let self else {
return
}
HapticFeedback().impact(.light)
#if DEBUG
switch attribute {
case .backdrop:
self.component?.externalState.testFailOrSuccess = true
case .pattern:
self.component?.externalState.testFailOrSuccess = false
default:
break
}
#endif
self.showAttributeInfo(tag: tag, text: tooltipText)
},
tag: tag
)
),
environment: {},
containerSize: availableSize
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.addSubview(itemView)
if !transition.animation.isImmediate {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
itemTransition.setFrame(view: itemView, frame: attributeDialFrame)
transition.setAlpha(view: itemView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: itemView.layer, radius: isCrafting ? 10.0 : 0.0)
}
attributeDialFrame.origin.x += attributeDialSize.width + attributeDialSpacing
attributeDialIndex += 1
if attributeDialIndex == firstRowCount {
attributeDialFrame.origin.x = floorToScreenPixels((availableSize.width - attributeSecondRowTotalWidth) / 2.0)
attributeDialFrame.origin.y += 66.0
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.attributeDials {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeIds {
self.attributeDials.removeValue(forKey: id)
}
if secondRowCount == 0, case .default = component.displayState {
let variantsString = environment.strings.Gift_Craft_ViewVariants
let variantsButtonMeasure = self.variantsButtonMeasure.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))),
environment: {},
containerSize: availableSize
)
let variantsButton: ComponentView<Empty>
if let current = self.variantsButton {
variantsButton = current
} else {
variantsButton = ComponentView<Empty>()
self.variantsButton = variantsButton
}
let variantsButtonSize = CGSize(width: variantsButtonMeasure.width + 87.0, height: 24.0)
if let gift = self.starGiftsMap[component.gift.giftId] {
var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
if let upgradePreview = self.upgradePreview {
var i = 0
for attribute in upgradePreview {
if case .model = attribute {
switch i {
case 0:
variant1 = .preview(attributes: [attribute], rarity: nil)
case 1:
variant2 = .preview(attributes: [attribute], rarity: nil)
case 2:
variant3 = .preview(attributes: [attribute], rarity: nil)
default:
break
}
i += 1
}
}
}
let _ = variantsButton.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: variantsButtonSize,
backgroundColor: component.colors.3,
isDark: true,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "content", component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "icon1", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant1,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "icon2", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant2,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "icon3", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: variant3,
isPlaceholder: false,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(id: "text", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .white)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white)
))
], spacing: 3.0))),
action: { [weak self] _ in
HapticFeedback().impact(.light)
self?.openUpgradeVariants()
}
)
),
environment: {},
containerSize: availableSize
)
}
let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight), size: variantsButtonSize)
var varitantsButtonTransition = transition
if let variantsButtonView = variantsButton.view {
if variantsButtonView.superview == nil {
varitantsButtonTransition = .immediate
if let descriptionView = self.descriptionText.view {
self.insertSubview(variantsButtonView, aboveSubview: descriptionView)
} else {
self.addSubview(variantsButtonView)
}
}
varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame)
}
} else if let variantsButton = self.variantsButton {
self.variantsButton = nil
if let variantsButtonView = variantsButton.view {
transition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0)
variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
variantsButtonView.removeFromSuperview()
})
}
}
craftContentHeight += 145.0
let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) })
if component.displayState == .crafting {
//var craftingOriginY = craftContentHeight * 0.5 + 160.0
var craftingOriginY = craftContentHeight * 0.5 - 16.0
let offset = -(craftContentHeight - originalCraftContentHeight)
let offset: CGFloat = 0.0
let craftingTitleSize = self.craftingTitle.update(
transition: transition,
@ -935,7 +1000,7 @@ private final class CraftGiftPageContent: Component {
if component.displayState == .failure {
var failureOriginY = craftContentHeight * 0.5 - 16.0
let offset = -(craftContentHeight - originalCraftContentHeight)
let offset: CGFloat = 0.0
let failureTitleSize = self.failureTitle.update(
transition: transition,
@ -1081,6 +1146,9 @@ private final class CraftGiftPageContent: Component {
guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else {
return
}
HapticFeedback().impact(.light)
let selectController = SelectCraftGiftScreen(
context: component.context,
craftContext: component.craftContext,
@ -1105,6 +1173,8 @@ private final class CraftGiftPageContent: Component {
guard let self else {
return
}
HapticFeedback().impact(.light)
self.component?.removeGift(index)
},
willFinish: { [weak self] success in
@ -1142,6 +1212,10 @@ private final class CraftGiftPageContent: Component {
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in
controller.dismiss()
})
HapticFeedback().success()
} else {
HapticFeedback().error()
}
}
)
@ -1482,7 +1556,7 @@ private final class SheetContainerComponent: CombinedComponent {
permilleValue += gift.gift.craftChancePermille ?? 0
}
}
if permilleValue > 900 {
if permilleValue >= 950 {
colors.0 = UIColor(rgb: 0x1b3b3d)
colors.1 = UIColor(rgb: 0x1a2f38)
colors.2 = UIColor(rgb: 0x22464a)
@ -1689,6 +1763,8 @@ private final class SheetContainerComponent: CombinedComponent {
state.displayInfo = false
state.updated(transition: .spring(duration: 0.3))
} else if state.displayFailure, let genericGift = externalState.starGiftsMap[component.gift.giftId] {
HapticFeedback().impact(.light)
let selectController = SelectCraftGiftScreen(
context: component.context,
craftContext: component.craftContext,
@ -1707,6 +1783,8 @@ private final class SheetContainerComponent: CombinedComponent {
)
environment.controller()?.push(selectController)
} else {
HapticFeedback().impact(.medium)
state.inProgress = true
state.updated(transition: .spring(duration: 0.3))
@ -1876,23 +1954,25 @@ public class GiftCraftScreen: ViewControllerComponentContainer {
private struct GiftCraftConfiguration {
static var defaultValue: GiftCraftConfiguration {
return GiftCraftConfiguration(
craftAttributePermilles: [60, 180, 450, 1000]
craftAttributePermilles: [[90], [80, 200], [70, 190, 460], [60, 180, 450, 1000]]
)
}
let craftAttributePermilles: [Int32]
let craftAttributePermilles: [[Int32]]
fileprivate init(
craftAttributePermilles: [Int32]
craftAttributePermilles: [[Int32]]
) {
self.craftAttributePermilles = craftAttributePermilles
}
static func with(appConfiguration: AppConfiguration) -> GiftCraftConfiguration {
if let data = appConfiguration.data {
var craftAttributePermilles: [Int32] = []
if let value = data["stargifts_craft_attribute_permilles"] as? [Double] {
craftAttributePermilles = value.map { Int32($0) }
var craftAttributePermilles: [[Int32]] = []
if let value = data["stargifts_craft_attribute_permilles"] as? [[Double]] {
craftAttributePermilles = value.map { innerArray in
innerArray.map { Int32($0) }
}
} else {
craftAttributePermilles = GiftCraftConfiguration.defaultValue.craftAttributePermilles
}
@ -1905,20 +1985,3 @@ private struct GiftCraftConfiguration {
}
}
}
private func animateRipple(parentView: UIView, screenCornerRadius: CGFloat, location: CGPoint) {
if let snapshotView = parentView.snapshotView(afterScreenUpdates: false) {
let wrappingNode = SpaceWarpNodeImpl()
wrappingNode.isUserInteractionEnabled = false
wrappingNode.frame = CGRect(origin: .zero, size: parentView.bounds.size)
wrappingNode.update(size: parentView.bounds.size, cornerRadius: screenCornerRadius, transition: .immediate)
parentView.addSubview(wrappingNode.view)
wrappingNode.contentNode.view.addSubview(snapshotView)
wrappingNode.triggerRipple(at: location)
Queue.mainQueue().after(0.7, {
wrappingNode.view.removeFromSuperview()
})
}
}

View file

@ -206,6 +206,8 @@ final class SelectGiftPageContent: Component {
guard let self, let component = self.component else {
return
}
HapticFeedback().impact(.light)
component.selectGift(gift)
component.dismiss()
},

View file

@ -172,6 +172,7 @@ public final class GiftItemComponent: Component {
let animateChanges: Bool
let mode: Mode
let cornerRadius: CGFloat?
let allowAnimations: Bool
let action: (() -> Void)?
let contextAction: ((UIView, ContextGesture) -> Void)?
@ -200,6 +201,7 @@ public final class GiftItemComponent: Component {
animateChanges: Bool = false,
mode: Mode = .generic,
cornerRadius: CGFloat? = nil,
allowAnimations: Bool = true,
action: (() -> Void)? = nil,
contextAction: ((UIView, ContextGesture) -> Void)? = nil
) {
@ -227,6 +229,7 @@ public final class GiftItemComponent: Component {
self.animateChanges = animateChanges
self.mode = mode
self.cornerRadius = cornerRadius
self.allowAnimations = allowAnimations
self.action = action
self.contextAction = contextAction
}
@ -304,6 +307,9 @@ public final class GiftItemComponent: Component {
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.allowAnimations != rhs.allowAnimations {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
@ -713,7 +719,7 @@ public final class GiftItemComponent: Component {
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0),
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
animationLayer.isVisibleForAnimations = component.allowAnimations
self.animationLayer = animationLayer
if let patternView = self.patternView.view {

View file

@ -882,6 +882,8 @@ public final class GiftStoreContentComponent: Component {
}
let previousFilterAttributes = self.starGiftsState?.filterAttributes
let previousSorting = self.starGiftsState?.sorting
let previousDataState = self.starGiftsState?.dataState
let previousItems = self.starGiftsState?.gifts
self.starGiftsState = state
var transition: ComponentTransition = .immediate
@ -890,6 +892,11 @@ public final class GiftStoreContentComponent: Component {
} else if let previousSorting, previousSorting != state.sorting {
transition = .easeInOut(duration: 0.25)
}
if previousItems?.isEmpty == true, case .loading = previousDataState, component.confirmPurchaseImmediately {
transition = .spring(duration: 0.3)
}
if !self.isUpdating {
self.state?.updated(transition: transition)
}

View file

@ -519,7 +519,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
title: title,
ribbon: nil,
isSelected: isSelected,
mode: .upgradePreview
mode: .upgradePreview,
allowAnimations: self.selectedSection != .backdrops
)
),
effectAlignment: .center,