Various improvements

This commit is contained in:
Ilya Laktyushin 2025-12-01 19:54:58 +04:00
parent b04eee8289
commit 6bc1fe1939
71 changed files with 4405 additions and 643 deletions

View file

@ -15434,3 +15434,35 @@ Error: %8$@";
"LiveStream.ErrorMaxAllowedEmoji.Text_any" = "You can send up to %d emoji.";
"Stars.Purchase.StarGiftOfferInfo" = "Buy Stars to make a gift buy offer.";
"Notification.StarsGiftOffer.Offer" = "%1$@ offered you %2$@ for your gift %3$@";
"Notification.StarsGiftOffer.Offer.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.Offer.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.OfferYou" = "You offered %1$@ %2$@ for their gift %3$@";
"Notification.StarsGiftOffer.OfferYou.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.OfferYou.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.Accepted" = "%1$@ accepted your offer and sold you %2$@ for %3$@";
"Notification.StarsGiftOffer.Accepted.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.Accepted.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.AcceptedYou" = "You sold %1$@ to %2$@ for %3$@";
"Notification.StarsGiftOffer.AcceptedYou.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.AcceptedYou.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.Rejected" = "%1$@ rejected your offer for %2$@ your %3$@ have been refunded";
"Notification.StarsGiftOffer.Rejected.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.Rejected.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.RejectedYou" = "You rejected %1$@'s offer";
"Notification.StarsGiftOffer.RejectedYou.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.RejectedYou.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.Expired" = "%1$@ didn't respond to your offer for %2$@ within %3$@ your %4$@ have been refunded";
"Notification.StarsGiftOffer.Expired.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.Expired.Stars_any" = "%@ Stars";
"Notification.StarsGiftOffer.ExpiredYou" = "The offer from %1$@ to buy your %2$@ for %3$@ has expired";
"Notification.StarsGiftOffer.ExpiredYou.Stars_1" = "%@ Star";
"Notification.StarsGiftOffer.ExpiredYou.Stars_any" = "%@ Stars";
"CreateExternalStream.RevokeStreamKey" = "Revoke Stream Key";
"CreateExternalStream.Revoke.Text" = "Do you want to revoke the stream key?";
"CreateExternalStream.Revoke.Revoke" = "Revoke";

View file

@ -1427,6 +1427,7 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController
func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController
func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController
func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController

View file

@ -13,14 +13,14 @@ enum BotCheckoutActionButtonState: Equatable {
private let titleFont = Font.semibold(17.0)
final class BotCheckoutActionButton: HighlightableButtonNode {
final class BotCheckoutActionButton: HighlightTrackingButtonNode {
static var height: CGFloat = 52.0
private var activeFillColor: UIColor
private var inactiveFillColor: UIColor
private var foregroundColor: UIColor
private let activeBackgroundNode: ASImageNode
private let activeBackgroundNode: ASDisplayNode
private var applePayButton: UIButton?
private let labelNode: TextNode
@ -29,23 +29,17 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
private var placeholderNode: ShimmerEffectNode?
private var activeImage: UIImage?
private var inactiveImage: UIImage?
init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) {
self.activeFillColor = activeFillColor
self.inactiveFillColor = inactiveFillColor
self.foregroundColor = foregroundColor
let diameter: CGFloat = 20.0
self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor)
let diameter: CGFloat = 52.0
self.activeBackgroundNode = ASImageNode()
self.activeBackgroundNode.displaysAsynchronously = false
self.activeBackgroundNode.displayWithoutProcessing = true
self.activeBackgroundNode = ASDisplayNode()
self.activeBackgroundNode.isLayerBacked = true
self.activeBackgroundNode.image = self.activeImage
self.activeBackgroundNode.backgroundColor = activeFillColor
self.activeBackgroundNode.cornerRadius = diameter / 2.0
self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false
@ -55,6 +49,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.addSubnode(self.activeBackgroundNode)
self.addSubnode(self.labelNode)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
let transition = ContainedViewLayoutTransition.animated(duration: highlighted ? 0.25 : 0.35, curve: .spring)
if highlighted {
let highlightedColor = self.activeFillColor.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
transition.updateBackgroundColor(node: self.activeBackgroundNode, color: highlightedColor)
transition.updateTransformScale(node: self, scale: 1.05)
} else {
transition.updateBackgroundColor(node: self.activeBackgroundNode, color: self.activeFillColor)
transition.updateTransformScale(node: self, scale: 1.0)
}
}
}
func setState(_ state: BotCheckoutActionButtonState) {
@ -93,14 +102,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
placeholderNode.removeFromSupernode()
}
let image = isEnabled ? self.activeImage : self.inactiveImage
if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image {
self.activeBackgroundNode.image = image
self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
} else {
self.activeBackgroundNode.image = image
}
self.activeBackgroundNode.backgroundColor = isEnabled ? self.activeFillColor : self.inactiveFillColor
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()

View file

@ -259,31 +259,31 @@ enum BotCheckoutEntry: ItemListNodeEntry {
}
})
case let .paymentMethod(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openPaymentMethod()
})
case let .shippingInfo(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.address(.street1))
})
case let .shippingMethod(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openShippingMethod()
})
case let .nameInfo(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.name)
})
case let .emailInfo(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.email)
})
case let .phoneInfo(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.phone)
})
case let .actionPlaceholder(_, shimmeringIndex):
return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
}, shimmeringIndex: shimmeringIndex)
}
}
@ -1245,7 +1245,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
let bottomPanelHorizontalInset: CGFloat = 16.0
let bottomPanelHorizontalInset: CGFloat = 30.0
var botName: String?
if let botPeer = self.botPeerValue {

View file

@ -270,7 +270,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: params.width, height: separatorHeight))

View file

@ -141,13 +141,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let naturalContentHeight: CGFloat
var verticalOffset: CGFloat = 0.0
if item.isFinal {
naturalContentHeight = 44.0
naturalContentHeight = 52.0
} else {
switch neighbors.bottom {
case .otherSection, .none:
naturalContentHeight = 44.0
naturalContentHeight = 52.0
default:
naturalContentHeight = 34.0
naturalContentHeight = 42.0
}
}
if let _ = previousItem as? BotCheckoutHeaderItem {
@ -209,7 +209,7 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))

View file

@ -70,15 +70,17 @@ final class BotPaymentActionItemNode: BotPaymentItemNode {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemAccentColor)
}
self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel)))
let height: CGFloat = 52.0
self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel)))
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 32.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
return 44.0
return height
}
@objc func buttonPressed() {

View file

@ -45,9 +45,9 @@ final class BotPaymentCardInputItemNode: BotPaymentItemNode, STPPaymentCardTextF
self.cardField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
}
self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 0.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0))
self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 4.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0))
return 44.0
return 52.0
}
func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) {

View file

@ -102,13 +102,15 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode {
}
}
self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel)))
let height: CGFloat = 52.0
self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel)))
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
var textInset = leftInset
if !titleSize.width.isZero {
@ -117,9 +119,9 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode {
textInset = max(measuredInset, textInset)
let textSize = self.textNode.measure(CGSize(width: width - measuredInset - 8.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 11.0), size: textSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 16.0), size: textSize))
return 44.0
return height
}
@objc func buttonPressed() {

View file

@ -108,11 +108,12 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
self.textField.textField.tintColor = theme.list.itemAccentColor
}
let height: CGFloat = 52.0
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
var textInset = leftInset
if !titleSize.width.isZero {
@ -121,9 +122,9 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
textInset = max(measuredInset, textInset)
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 5.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
return 44.0
return height
}
func activateInput() {

View file

@ -74,7 +74,7 @@ class BotPaymentItemNode: ASDisplayNode {
self.bottomSeparatorNode.isHidden = hasCorners
}
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.maskNode, frame: self.backgroundNode.frame.insetBy(dx: sideInset, dy: 0.0))

View file

@ -74,14 +74,14 @@ final class BotPaymentSwitchItemNode: BotPaymentItemNode {
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
let switchSize = self.switchNode.measure(CGSize(width: 100.0, height: 100.0))
let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 6.0), size: switchSize)
let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 12.0), size: switchSize)
transition.updateFrame(node: self.switchNode, frame: switchFrame)
transition.updateFrame(node: self.buttonNode, frame: switchFrame)
return 44.0
return 52.0
}
@objc private func buttonPressed() {

View file

@ -335,7 +335,7 @@ final class BotReceiptControllerNode: ItemListControllerNode {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
let bottomPanelHorizontalInset: CGFloat = 16.0
let bottomPanelHorizontalInset: CGFloat = 30.0
let bottomPanelVerticalInset: CGFloat = 16.0
let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height

View file

@ -1425,9 +1425,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
var nodeList = document.getElementsByTagName('link');
for (var i = 0; i < nodeList.length; i++)
{
if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon')))
{
const node = nodeList[i];
var rel = nodeList[i].getAttribute('rel') || '';
if (
rel === 'icon' ||
rel === 'shortcut icon' ||
rel.indexOf('apple-touch-icon') === 0
) {
var node = nodeList[i];
favicons.push({
url: node.getAttribute('href'),
sizes: node.getAttribute('sizes')

View file

@ -157,40 +157,6 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
}
final class BackgroundView: UIView {
let topCornersView = UIView()
let bottomCornersView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.topCornersView.clipsToBounds = true
self.topCornersView.layer.cornerCurve = .continuous
self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.bottomCornersView.clipsToBounds = true
self.bottomCornersView.layer.cornerCurve = .continuous
self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.addSubview(self.topCornersView)
self.topCornersView.addSubview(self.bottomCornersView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
transition.setBackgroundColor(view: self.bottomCornersView, color: color)
}
}
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView {
public final class Tag {
public init() {
@ -208,9 +174,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
private let dimView: UIView
private let scrollView: ScrollView
private let backgroundView: BackgroundView
private let backgroundView: SheetBackgroundView
private var effectView: UIVisualEffectView?
private let clipView: BackgroundView
private let clipView: SheetBackgroundView
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
@ -233,8 +199,8 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceVertical = true
self.backgroundView = BackgroundView()
self.clipView = BackgroundView()
self.backgroundView = SheetBackgroundView()
self.clipView = SheetBackgroundView()
self.contentView = ComponentView<ChildEnvironmentType>()
@ -589,3 +555,37 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class SheetBackgroundView: UIView {
let topCornersView = UIView()
let bottomCornersView = UIView()
public override init(frame: CGRect) {
super.init(frame: frame)
self.topCornersView.clipsToBounds = true
self.topCornersView.layer.cornerCurve = .continuous
self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.bottomCornersView.clipsToBounds = true
self.bottomCornersView.layer.cornerCurve = .continuous
self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.addSubview(self.topCornersView)
self.topCornersView.addSubview(self.bottomCornersView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
transition.setBackgroundColor(view: self.bottomCornersView, color: color)
}
}

View file

@ -371,7 +371,7 @@ class GeneralChartComponentController: ChartThemeContainer {
values.append(ChartDetailsViewModel.Value(
prefix: nil,
title: self.strings.revenueInUsd,
value: "$\(convertedValueString)",
value: "~$\(convertedValueString)",
color: color,
visible: firstValue.visible
))

View file

@ -161,7 +161,7 @@ class BarsComponentController: GeneralChartComponentController {
} else {
text = String(format: "%0.3f", convertedValue)
}
updatedLabels.append(LinesChartLabel(value: label.value, text: "$\(text)"))
updatedLabels.append(LinesChartLabel(value: label.value, text: "~$\(text)"))
}
labels = updatedLabels
}

View file

@ -494,7 +494,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
var label: String = ""
if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero {
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
label = presentationData.strings.InviteLink_Create_FeePerMonth("\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
label = presentationData.strings.InviteLink_Create_FeePerMonth("~\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
}
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0)))
}

View file

@ -44,7 +44,7 @@ public final class ListMessageItemInteraction {
})
}
public final class ListMessageItem: ListViewItem {
public final class ListMessageItem: ListViewItem, ItemListItem {
let presentationData: ChatPresentationData
let systemStyle: ItemListSystemStyle
let context: AccountContext
@ -57,6 +57,7 @@ public final class ListMessageItem: ListViewItem {
let isGlobalSearchResult: Bool
let isDownloadList: Bool
let isSavedMusic: Bool
let isStoryMusic: Bool
let displayFileInfo: Bool
let displayBackground: Bool
let canReorder: Bool
@ -64,9 +65,11 @@ public final class ListMessageItem: ListViewItem {
let header: ListViewItemHeader?
public var sectionId: ItemListSectionId
public let selectable: Bool = true
public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) {
public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, isStoryMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.context = context
@ -86,10 +89,12 @@ public final class ListMessageItem: ListViewItem {
self.isGlobalSearchResult = isGlobalSearchResult
self.isDownloadList = isDownloadList
self.isSavedMusic = isSavedMusic
self.isStoryMusic = isStoryMusic
self.displayFileInfo = displayFileInfo
self.displayBackground = displayBackground
self.canReorder = canReorder
self.style = style
self.sectionId = sectionId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -120,7 +125,23 @@ public final class ListMessageItem: ListViewItem {
node.setupItem(self)
let nodeLayout = node.asyncLayout()
let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
var topMerged = false
if let previousItem {
if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain {
topMerged = true
}
}
var bottomMerged = false
if let nextItem {
if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain {
bottomMerged = true
}
}
let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
node.updateSelectionState(animated: false)
@ -152,9 +173,22 @@ public final class ListMessageItem: ListViewItem {
let nodeLayout = nodeValue.asyncLayout()
var topMerged = false
if let previousItem {
if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain {
topMerged = true
}
}
var bottomMerged = false
if let nextItem {
if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain {
bottomMerged = true
}
}
async {
let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
Queue.mainQueue().async {
completion(layout, { _ in

View file

@ -446,9 +446,9 @@ public func storyLocationPickerController(
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
controller.requestController = { _, present in
let locationPickerController = LocationPickerController(context: context, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in
let locationPickerController = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in
completion(location, queryId, resultId, address, countryCode)
})
present(locationPickerController, locationPickerController.mediaPickerContext)

View file

@ -19,7 +19,7 @@ public func mediaPasteboardScreen(
getSourceRect: (() -> CGRect?)? = nil,
makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil }
) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView)
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView)
controller.requestController = { _, present in
presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in
present(mediaPicker, mediaPickerContext)

View file

@ -26,6 +26,9 @@ swift_library(
"//submodules/Components/ActivityIndicatorComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",

View file

@ -14,6 +14,9 @@ import BundleIconComponent
import AnimatedStickerComponent
import ActivityIndicatorComponent
import GlassBarButtonComponent
import ListSectionComponent
import ListActionItemComponent
import PlainButtonComponent
private final class CreateExternalMediaStreamScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -49,41 +52,51 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
final class State: ComponentState {
let context: AccountContext
let peerId: EnginePeer.Id
let mode: CreateExternalMediaStreamScreen.Mode
private(set) var credentials: GroupCallStreamCredentials?
var isDelayingLoadingIndication: Bool = true
private var credentialsDisposable: Disposable?
private let credentialsDisposable = MetaDisposable()
private let activeActionDisposable = MetaDisposable()
init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
self.context = context
self.peerId = peerId
self.mode = mode
super.init()
self.getCredentials(credentialsPromise: credentialsPromise)
}
deinit {
self.credentialsDisposable.dispose()
self.activeActionDisposable.dispose()
}
func getCredentials(credentialsPromise: Promise<GroupCallStreamCredentials>? = nil, revoke: Bool = false) {
let credentialsSignal: Signal<GroupCallStreamCredentials, NoError>
if let credentialsPromise = credentialsPromise {
credentialsSignal = credentialsPromise.get()
} else {
credentialsSignal = context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, isLiveStream: false, revokePreviousCredentials: false)
var isLiveStream = false
if case let .create(isLiveStreamValue) = self.mode {
isLiveStream = isLiveStreamValue
}
credentialsSignal = self.context.engine.calls.getGroupCallStreamCredentials(peerId: self.peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revoke)
|> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in
return .never()
}
}
self.credentialsDisposable = (credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in
self.credentialsDisposable.set((credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.credentials = result
strongSelf.updated(transition: .immediate)
})
}
deinit {
self.credentialsDisposable?.dispose()
self.activeActionDisposable.dispose()
}))
}
func copyCredentials(_ key: KeyPath<GroupCallStreamCredentials, String>) {
@ -160,7 +173,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId, credentialsPromise: self.credentialsPromise)
return State(context: self.context, peerId: self.peerId, mode: self.mode, credentialsPromise: self.credentialsPromise)
}
static var body: Body {
@ -176,26 +189,25 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let activityIndicator = Child(ActivityIndicatorComponent.self)
let credentialsBackground = Child(RoundedRectangle.self)
let credentialsSection = Child(ListSectionComponent.self)
let credentialsStripe = Child(Rectangle.self)
let credentialsURLTitle = Child(MultilineTextComponent.self)
let credentialsURLText = Child(MultilineTextComponent.self)
let credentialsKeyTitle = Child(MultilineTextComponent.self)
let credentialsKeyText = Child(MultilineTextComponent.self)
let credentialsCopyURLButton = Child(Button.self)
let credentialsCopyKeyButton = Child(Button.self)
// let credentialsBackground = Child(RoundedRectangle.self)
// let credentialsStripe = Child(Rectangle.self)
// let credentialsURLTitle = Child(MultilineTextComponent.self)
// let credentialsURLText = Child(MultilineTextComponent.self)
//
// let credentialsKeyTitle = Child(MultilineTextComponent.self)
// let credentialsKeyText = Child(MultilineTextComponent.self)
//
// let credentialsCopyURLButton = Child(Button.self)
// let credentialsCopyKeyButton = Child(Button.self)
return { context in
let topInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0
let buttonSideInset: CGFloat = 36.0
let credentialsSideInset: CGFloat = 16.0
let credentialsTopInset: CGFloat = 12.0
let credentialsTitleSpacing: CGFloat = 5.0
let component = context.component
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
@ -355,124 +367,145 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
)
let textFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - text.size.width) / 2.0), y: animationFrame.maxY + 18.0), size: text.size)
context.add(text
.position(CGPoint(x: textFrame.midX, y: textFrame.midY))
)
let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize)
if let credentials = context.state.credentials {
let credentialsURLTitle = credentialsURLTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
transition: context.transition
var credentialsSectionItems: [AnyComponentWithIdentity<Empty>] = []
credentialsSectionItems.append(
AnyComponentWithIdentity(id: "url", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.CreateExternalStream_ServerUrl,
font: Font.regular(15.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .left, spacing: 5.0)),
contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "copy",
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.copyCredentials(\.url)
},
animateScale: false
)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
let credentialsKeyTitle = credentialsKeyTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
transition: context.transition
credentialsSectionItems.append(
AnyComponentWithIdentity(id: "key", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.CreateExternalStream_StreamKey,
font: Font.regular(15.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .left, spacing: 5.0)),
contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "copy",
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.copyCredentials(\.streamKey)
},
animateScale: false
)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
let credentialsURLText = credentialsURLText.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsKeyText = credentialsKeyText.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 48.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsBackground = credentialsBackground.update(
component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0),
availableSize: credentialsAreaSize,
transition: context.transition
)
let credentialsStripe = credentialsStripe.update(
component: Rectangle(color: theme.list.itemPlainSeparatorColor),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: UIScreenPixel),
transition: context.transition
)
let credentialsCopyURLButton = credentialsCopyURLButton.update(
component: Button(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
credentialsSectionItems.append(
AnyComponentWithIdentity(id: "revoke", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_RevokeStreamKey, font: Font.regular(17.0), textColor: theme.list.itemDestructiveColor)),
horizontalAlignment: .center,
truncationType: .middle,
maximumNumberOfLines: 1
)),
titleAlignment: .center,
action: { [weak state] _ in
guard let state = state else {
return
}
let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in
state?.getCredentials(revoke: true)
})])
environment.controller()?.present(alertController, in: .window(.root))
}
state.copyCredentials(\.url)
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 44.0, height: 44.0),
)
))
)
let credentialsSection = credentialsSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: credentialsSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
let credentialsCopyKeyButton = credentialsCopyKeyButton.update(
component: Button(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.copyCredentials(\.streamKey)
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(credentialsBackground
.position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY))
)
context.add(credentialsStripe
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsStripe.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight))
)
context.add(credentialsURLTitle
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsURLTitle.size.height / 2.0))
)
context.add(credentialsURLText
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLText.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsTitleSpacing + credentialsURLTitle.size.height + credentialsURLText.size.height / 2.0))
)
context.add(credentialsCopyURLButton
.position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyURLButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight / 2.0))
)
context.add(credentialsKeyTitle
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsKeyTitle.size.height / 2.0))
)
context.add(credentialsKeyText
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyText.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsTitleSpacing + credentialsKeyTitle.size.height + credentialsKeyText.size.height / 2.0))
)
context.add(credentialsCopyKeyButton
.position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyKeyButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsItemHeight / 2.0))
)
context.add(credentialsSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: textFrame.maxY + 30.0 + credentialsSection.size.height / 2.0)))
} else if !context.state.isDelayingLoadingIndication {
let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize)
let activityIndicator = activityIndicator.update(
component: ActivityIndicatorComponent(color: theme.list.controlSecondaryColor),
availableSize: CGSize(width: 100.0, height: 100.0),

View file

@ -732,7 +732,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
var price: String = ""
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
price = "\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
price = "~\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice))
entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price))

View file

@ -194,7 +194,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
let price = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
let price = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium))
entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price))

View file

@ -180,10 +180,10 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
case .ton:
let cryptoValue = formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat)
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
case .stars:
amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance.amount, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
isStars = true
}
} else {

View file

@ -443,7 +443,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { "\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "",
item.publicShares.flatMap { "~\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
.generic
@ -485,7 +485,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { "\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "",
item.publicShares.flatMap { "~\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
.generic
@ -512,7 +512,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
"\(Int(stats.premiumAudience?.value ?? 0))",
"~\(Int(stats.premiumAudience?.value ?? 0))",
item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic),
.generic
@ -770,7 +770,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Available,
(stats.balances.availableBalance.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -780,7 +780,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Current,
(stats.balances.currentBalance.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -790,7 +790,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Total,
(stats.balances.overallRevenue.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -800,7 +800,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@ -810,7 +810,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@ -820,7 +820,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@ -834,7 +834,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Available,
(stats.balances.availableBalance.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -844,7 +844,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Current,
(stats.balances.currentBalance.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -854,7 +854,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Total,
(stats.balances.overallRevenue.amount.value == 0 ? "" : "\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@ -869,7 +869,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Available,
(stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@ -879,7 +879,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Current,
(stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@ -889,7 +889,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Total,
(stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
(stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)

View file

@ -25,10 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
public let entities: [MessageTextEntity]
public let toLang: String
public let additional:[Additional]
public let additional: [Additional]
public let pollSolution: Additional?
public var associatedPeerIds: [PeerId] {
return []
}

View file

@ -593,7 +593,7 @@ public extension TelegramEngine {
}
public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang)
return _internal_translateTexts(network: self.account.network, texts: texts, toLang: toLang)
}
public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Never, TranslationError> {

View file

@ -48,7 +48,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie
}
}
func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
@ -112,9 +112,9 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
return .never()
}
let polls = messages.compactMap { msg in
if let poll = msg.media.first as? TelegramMediaPoll {
return (poll, msg.id)
let polls = messages.compactMap { message in
if let poll = message.media.first as? TelegramMediaPoll {
return (poll, message.id)
} else {
return nil
}
@ -128,9 +128,19 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
if let solution = poll.results.solution {
texts.append((solution.text, solution.entities))
}
return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang)
return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang)
}
let audioTranscriptions = messages.compactMap { message in
if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
return (audioTranscription.text, message.id)
} else {
return nil
}
}
let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in
return _internal_translate(network: account.network, text: text, toLang: toLang)
}
var flags: Int32 = 0
flags |= (1 << 0)
@ -197,8 +207,8 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
}
}
return combineLatest(msgs, combineLatest(pollSignals))
|> mapToSignal { (result, pollResults) -> Signal<Void, TranslationError> in
return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals))
|> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal<Void, TranslationError> in
return account.postbox.transaction { transaction in
if case let .translateResult(results) = result {
var index = 0
@ -218,6 +228,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
index += 1
}
}
if !pollResults.isEmpty {
for (i, poll) in polls.enumerated() {
let result = pollResults[i]
@ -231,12 +242,12 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
if translated.0.isEmpty {
translated = (poll.0.options[i].text, poll.0.options[i].entities)
}
attrOptions.append(.init(text: translated.0, entities: translated.1))
attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1))
}
let solution: TranslationMessageAttribute.Additional?
if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty {
solution = .init(text: result[result.count - 1].0, entities: result[result.count - 1].1)
solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1)
} else {
solution = nil
}
@ -251,6 +262,22 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
}
}
}
if !audioTranscriptionsResults.isEmpty {
for (i, audioTranscription) in audioTranscriptions.enumerated() {
if let result = audioTranscriptionsResults[i] {
transaction.updateMessage(audioTranscription.1, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang)
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
}
}
|> castError(TranslationError.self)
}

View file

@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
@ -1290,7 +1290,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
}
} else {
if message.id.peerId == accountPeerId && assigned {
if fromOffer, let resaleStars {
let starsString: String
switch resaleStars.currency {
case .stars:
starsString = strings.Notification_StarsGiftOffer_Accepted_Stars(Int32(clamping: resaleStars.amount.value))
case .ton:
starsString = formatTonAmountText(resaleStars.amount.value, dateTimeFormat: dateTimeFormat) + " TON"
}
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
var peerName = ""
if let name = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
peerName = name
}
if message.author?.id == accountPeerId {
let peerIds: [(Int, EnginePeer.Id?)] = [(1, message.id.peerId)]
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[0] = boldAttributes
attributes[2] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_AcceptedYou(giftTitle, peerName, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributes[2] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Accepted(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else if message.id.peerId == accountPeerId && assigned {
let attributes: [Int: MarkdownAttributeSet] = [0: boldAttributes]
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Assigned(giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
@ -1616,10 +1642,103 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case .starGiftPurchaseOffer:
attributedString = nil
case .starGiftPurchaseOfferDeclined:
attributedString = nil
case let .starGiftPurchaseOffer(gift, amount, _, _, _):
let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let giftTitle: String
if case let .unique(gift) = gift {
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
} else {
giftTitle = ""
}
let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)]
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributes[2] = boldAttributes
if message.author?.id == accountPeerId {
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Notification_StarsGiftOffer_OfferYou_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_OfferYou(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Notification_StarsGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Offer(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired):
let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)]
let giftTitle: String
if case let .unique(gift) = gift {
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
} else {
giftTitle = ""
}
if hasExpired {
if message.author?.id == accountPeerId {
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Notification_StarsGiftOffer_ExpiredYou_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributes[2] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_ExpiredYou(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Notification_StarsGiftOffer_Expired_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
let timeString = "[TODO]"
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributes[2] = boldAttributes
attributes[3] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Expired(peerName, giftTitle, timeString, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
if message.author?.id == accountPeerId {
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_RejectedYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Notification_StarsGiftOffer_Rejected_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributes[2] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Rejected(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case .unknown:
attributedString = nil
}

View file

@ -402,6 +402,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode",
"//submodules/TelegramUI/Components/Chat/ChatRecentActionsController",

View file

@ -18,23 +18,29 @@ import ComponentFlow
import GlassBarButtonComponent
import BundleIconComponent
import EdgeEffect
import SaveToCameraRoll
private final class AttachmentFileControllerArguments {
let context: AccountContext
let isAudio: Bool
let openGallery: () -> Void
let openFiles: () -> Void
let expandSavedMusic: () -> Void
let send: (Message) -> Void
init(context: AccountContext, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, send: @escaping (Message) -> Void) {
init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
self.isAudio = isAudio
self.openGallery = openGallery
self.openFiles = openFiles
self.expandSavedMusic = expandSavedMusic
self.send = send
}
}
private enum AttachmentFileSection: Int32 {
case select
case savedMusic
case recent
}
@ -55,6 +61,10 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromGallery(PresentationTheme, String)
case selectFromFiles(PresentationTheme, String)
case savedHeader(PresentationTheme, String)
case savedFile(Int32, PresentationTheme, Message?)
case showMore(PresentationTheme, String)
case recentHeader(PresentationTheme, String)
case file(Int32, PresentationTheme, Message?)
@ -62,6 +72,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
switch self {
case .selectFromGallery, .selectFromFiles:
return AttachmentFileSection.select.rawValue
case .savedHeader, .savedFile, .showMore:
return AttachmentFileSection.savedMusic.rawValue
case .recentHeader, .file:
return AttachmentFileSection.recent.rawValue
}
@ -73,10 +85,16 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
return 0
case .selectFromFiles:
return 1
case .recentHeader:
case .savedHeader:
return 2
case let .file(index, _, _):
case let .savedFile(index, _, _):
return 3 + index
case .showMore:
return 9999
case .recentHeader:
return 10000
case let .file(index, _, _):
return 10001 + index
}
}
@ -94,6 +112,24 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
} else {
return false
}
case let .savedHeader(lhsTheme, lhsText):
if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .savedFile(lhsIndex, lhsTheme, lhsMessage):
if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
}
case let .showMore(lhsTheme, lhsText):
if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .recentHeader(lhsTheme, lhsText):
if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -124,6 +160,22 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openFiles()
})
case let .savedHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .savedFile(_, _, message):
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
case let .showMore(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSavedMusic()
})
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .file(_, _, message):
@ -135,22 +187,53 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, displayFileInfo: false, displayBackground: true, style: .blocks)
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
}
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
guard !empty else {
return []
}
var entries: [AttachmentFileEntry] = []
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
if case .recent = mode {
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
}
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
let listTitle: String
switch mode {
case .recent:
listTitle = presentationData.strings.Attachment_RecentlySentFiles
case .audio:
//TODO:localize
listTitle = "SHARED AUDIO"
}
if case .audio = mode {
if let savedMusic, savedMusic.count > 0 {
entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased()))
var savedMusic = savedMusic
var showMore = false
if savedMusic.count > 4 && !state.savedMusicExpanded {
savedMusic = Array(savedMusic.prefix(3))
showMore = true
}
var i: Int32 = 0
for file in savedMusic {
entries.append(.savedFile(i, presentationData.theme, file))
i += 1
}
if showMore {
entries.append(.showMore(presentationData.theme, "Show More"))
}
}
}
if let recentDocuments = recentDocuments {
if recentDocuments.count > 0 {
entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
var i: Int32 = 0
for file in recentDocuments {
entries.append(.file(i, presentationData.theme, file))
@ -158,7 +241,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
}
}
} else {
entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
for i in 0 ..< 11 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
@ -185,6 +268,8 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon
var delayDisappear = false
var hasBottomEdgeEffect = true
var resetForReuseImpl: () -> Void = {}
public func resetForReuse() {
self.resetForReuseImpl()
@ -229,31 +314,41 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon
transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame)
topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
let bottomEdgeEffectView: EdgeEffectView
if let current = self.bottomEdgeEffectView {
bottomEdgeEffectView = current
} else {
bottomEdgeEffectView = EdgeEffectView()
self.view.addSubview(bottomEdgeEffectView)
self.bottomEdgeEffectView = bottomEdgeEffectView
if self.hasBottomEdgeEffect {
let bottomEdgeEffectView: EdgeEffectView
if let current = self.bottomEdgeEffectView {
bottomEdgeEffectView = current
} else {
bottomEdgeEffectView = EdgeEffectView()
self.view.addSubview(bottomEdgeEffectView)
self.bottomEdgeEffectView = bottomEdgeEffectView
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
} else if let bottomEdgeEffectView = self.bottomEdgeEffectView {
bottomEdgeEffectView.removeFromSuperview()
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
}
}
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
var savedMusicExpanded: Bool
}
public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController {
public enum AttachmentFileControllerMode {
case recent
case audio
}
public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: AttachmentFileControllerMode = .recent, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController {
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true)
let stateValue = Atomic(value: AttachmentFileControllerState(searching: false))
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true)
let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false))
let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
@ -265,39 +360,87 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
var updateIsSearchingImpl: ((Bool) -> Void)?
let arguments = AttachmentFileControllerArguments(
context: context,
isAudio: mode == .audio,
openGallery: {
presentGallery()
},
openFiles: {
presentFiles()
},
expandSavedMusic: {
updateState { state in
var updatedState = state
updatedState.savedMusicExpanded = true
return updatedState
}
},
send: { message in
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|> `catch` { _ in
return .single(.result([]))
}
|> mapToSignal { result -> Signal<[Message], NoError> in
guard case let .result(result) = result else {
return .complete()
if message.id.namespace == Namespaces.Message.Local {
if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.standalone(media: file))
}
return .single(result)
}
|> deliverOnMainQueue).startStandalone(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.message(message: MessageReference(message), media: file))
} else {
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|> `catch` { _ in
return .single(.result([]))
}
dismissImpl?()
})
|> mapToSignal { result -> Signal<[Message], NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> deliverOnMainQueue).startStandalone(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.message(message: MessageReference(message), media: file))
}
dismissImpl?()
})
}
}
)
let recentDocuments: Signal<[Message]?, NoError> = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
let recentDocuments: Signal<[Message]?, NoError>
let savedMusicContext: ProfileSavedMusicContext?
let savedMusic: Signal<[Message]?, NoError>
switch mode {
case .recent:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = nil
savedMusic = .single(nil)
case .audio:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId)
savedMusic = .single(nil)
|> then(
savedMusicContext!.state
|> map { state in
let peerId = context.account.peerId
var messages: [Message] = []
let peers = SimpleDictionary<PeerId, Peer>()
// if let peer {
// peers[peerId] = peer._asPeer()
// }
for file in state.files {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
return messages
}
)
}
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
@ -306,14 +449,12 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
let previousRecentDocuments = Atomic<[Message]?>(value: nil)
let signal = combineLatest(queue: Queue.mainQueue(),
presentationData,
recentDocuments,
statePromise.get()
)
|> map {
presentationData,
recentDocuments,
state -> (ItemListControllerState, (ItemListNodeState, Any)) in
savedMusic,
statePromise.get()
)
|> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
@ -397,9 +538,17 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
}
let title: String
switch mode {
case .recent:
title = presentationData.strings.Attachment_File
case .audio:
title = "Audio"
}
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(presentationData.strings.Attachment_File),
title: .text(title),
leftNavigationButton: leftNavigationButton,
rightNavigationButton: rightNavigationButton,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
@ -424,7 +573,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
var searchItem: ItemListControllerSearch?
if state.searching {
searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, focus: {
searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: {
expandImpl?()
}, cancel: {
updateState { state in
@ -441,14 +590,18 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
})
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
let _ = savedMusicContext?.state
}
let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true)
if case .audio = mode {
controller.hasBottomEdgeEffect = false
}
controller.delayDisappear = true
controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
switch offset {
@ -490,3 +643,34 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
}
return controller
}
public func storyAudioPickerController(
context: AccountContext,
selectFromFiles: @escaping () -> Void,
dismissed: @escaping () -> Void,
completion: @escaping (AnyMediaReference) -> Void,
) -> ViewController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
controller.requestController = { _, present in
let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: {
selectFromFiles()
dismissImpl?()
}, send: { file in
completion(file)
dismissImpl?()
}) as! AttachmentFileControllerImpl
present(filePickerController, filePickerController.mediaPickerContext)
}
controller.navigationPresentation = .flatModal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.didDismiss = {
dismissed()
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
return controller
}

View file

@ -21,6 +21,7 @@ import SearchInputPanelComponent
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
let mode: AttachmentFileControllerMode
let presentationData: PresentationData
let focus: () -> Void
let cancel: () -> Void
@ -31,8 +32,9 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
@ -69,7 +71,7 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return AttachmentFileSearchItemNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, dismissInput: self.dismissInput)
}
@ -77,8 +79,8 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private let theme: PresentationTheme
private let strings: PresentationStrings
private let mode: AttachmentFileControllerMode
private let presentationData: PresentationData
private let focus: () -> Void
private let cancel: () -> Void
@ -88,14 +90,14 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private var validLayout: ContainerViewLayout?
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.containerNode = AttachmentFileSearchContainerNode(context: context, forceTheme: nil, send: { message in
self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in
send(message)
}, updateActivity: updateActivity)
@ -136,15 +138,16 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition)
//TODO:localize
let searchInputSize = self.searchInput.update(
transition: .immediate,
component: AnyComponent(
SearchInputPanelComponent(
theme: self.theme,
strings: self.strings,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
metrics: layout.metrics,
safeInsets: layout.safeInsets,
placeholder: self.strings.Attachment_FilesSearchPlaceholder,
placeholder: self.mode == .audio ? "Search shared audio" : self.presentationData.strings.Attachment_FilesSearchPlaceholder,
updated: { [weak self] query in
guard let self else {
return
@ -251,7 +254,7 @@ private final class AttachmentFileSearchEntry: Comparable, Identifiable {
interaction.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
return ListMessageItem(presentationData: ChatPresentationData(presentationData: interaction.context.sharedContext.currentPresentationData.with({$0})), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
}
}
@ -293,10 +296,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private var _hasDim: Bool = false
@ -304,17 +305,12 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return _hasDim
}
public init(context: AccountContext, forceTheme: PresentationTheme?, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
self.context = context
self.send = send
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.forceTheme = forceTheme
if let forceTheme = self.forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
@ -380,13 +376,26 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return .single(nil)
}
let signal: Signal<[Message]?, NoError> = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
let signal: Signal<[Message]?, NoError>
switch mode {
case .recent:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
case .audio:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
}
updateActivity(true)
return combineLatest(signal, presentationDataPromise.get())
@ -420,25 +429,19 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
if let strongSelf = self {
var presentationData = presentationData
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
if let forceTheme = strongSelf.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
}
})
// self.presentationDataDisposable = (context.sharedContext.presentationData
// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
// if let strongSelf = self {
// let previousTheme = strongSelf.presentationData.theme
// let previousStrings = strongSelf.presentationData.strings
//
// strongSelf.presentationData = presentationData
//
// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
// }
// }
// })
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()

View file

@ -80,6 +80,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode",

View file

@ -70,6 +70,7 @@ import ChatMessageStoryMentionContentNode
import ChatMessageUnsupportedBubbleContentNode
import ChatMessageWallpaperBubbleContentNode
import ChatMessageGiftBubbleContentNode
import ChatMessageGiftOfferBubbleContentNode
import ChatMessageGiveawayBubbleContentNode
import ChatMessageJoinedChannelBubbleContentNode
import ChatMessageFactCheckBubbleContentNode
@ -246,6 +247,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .suggestedBirthday = action.action {
result.append((message, ChatMessageBirthdateSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .starGiftPurchaseOffer = action.action {
result.append((message, ChatMessageGiftOfferBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else {
if !canAddMessageReactions(message: message) {
needReactions = false
@ -2821,6 +2824,48 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
actionButtonsFinalize = buttonsLayout
lastNodeTopPosition = .None(.Both)
} else if incoming, let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(_, _, expireDate, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if expireDate <= currentTimestamp {
} else {
var buttonDeclineValue: UInt8 = 0
let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1))
var buttonApproveValue: UInt8 = 1
let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1))
let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [
buttonDecline: ChatMessageActionButtonsNode.CustomInfo(
isEnabled: true,
icon: .suggestedPostReject
),
buttonApprove: ChatMessageActionButtonsNode.CustomInfo(
isEnabled: true,
icon: .suggestedPostApprove
)
]
//TODO:localize
let (minWidth, buttonsLayout) = actionButtonsLayout(
item.context,
item.presentationData.theme,
item.presentationData.chatBubbleCorners,
item.presentationData.strings,
item.controllerInteraction.presentationContext.backgroundNode,
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: "Reject", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: "Accept", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
])
],
flags: [],
placeholder: nil
), customInfos, item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout
lastNodeTopPosition = .None(.Both)
}
} else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil {
var canApprove = true
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) {
@ -4772,7 +4817,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
var actionButtonsOriginX = backgroundFrame.minX
if case .center = alignment {
actionButtonsOriginX += 3.0
} else {
actionButtonsOriginX += incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { [weak strongSelf] button, progress in

View file

@ -430,6 +430,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var buttonIcon: String?
var ribbonTitle = ""
var customRibbonColors: [UIColor]?
var textSpacing: CGFloat = 0.0
var isStarGift = false
@ -686,7 +687,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer):
if case let .unique(uniqueGift) = gift {
isStarGift = true
@ -715,7 +716,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**"
ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
if fromOffer {
//TODO:localize
ribbonTitle = incoming ? "" : "sold"
customRibbonColors = [UIColor(rgb: 0xd9433a), UIColor(rgb: 0xff645b)]
} else {
ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
}
buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View
modelTitle = item.presentationData.strings.Notification_StarGift_Model
backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop
@ -1285,7 +1292,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if ribbonTextLayout.size.width > 0.0 {
if strongSelf.ribbonBackgroundNode.image == nil {
if let uniqueBackgroundColor {
if let customRibbonColors {
strongSelf.ribbonBackgroundNode.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: customRibbonColors, direction: .mirroredDiagonal)
} else if let uniqueBackgroundColor {
let colors = [
uniqueBackgroundColor.withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
uniqueBackgroundColor.withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)

View file

@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageGiftOfferBubbleContentNode",
module_name = "ChatMessageGiftOfferBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/LocalizedPeerData",
"//submodules/UrlEscaping",
"//submodules/TelegramStringFormatting",
"//submodules/WallpaperBackgroundNode",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,341 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import ComponentFlow
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import LocalizedPeerData
import UrlEscaping
import TelegramStringFormatting
import WallpaperBackgroundNode
import ReactionSelectionNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ChatControllerInteraction
import ShimmerEffect
import Markdown
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import TextNodeWithEntities
import GiftItemComponent
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
}
public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode
private let subtitleNode: TextNodeWithEntities
private let giftIcon = ComponentView<Empty>()
private var absoluteRect: (CGRect, CGSize)?
private var isPlaying: Bool = false
override public var disablesClipping: Bool {
return true
}
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.visibilityStatus = isVisible
switch self.visibility {
case .none:
self.subtitleNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.subtitleNode.visibilityRect = subRect
}
}
}
}
private var visibilityStatus: Bool? {
didSet {
if self.visibilityStatus != oldValue {
self.updateVisibility()
}
}
}
private var fetchDisposable: Disposable?
private var setupTimestamp: Double?
private var cachedTonImage: (UIImage, UIColor)?
required public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.subtitleNode = TextNodeWithEntities()
self.subtitleNode.textNode.isUserInteractionEnabled = false
self.subtitleNode.textNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode.textNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable?.dispose()
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var giftSize = CGSize(width: 260.0, height: 240.0)
var uniqueGift: StarGift.UniqueGift?
let incoming: Bool
if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil {
incoming = true
} else {
incoming = item.message.effectivelyIncoming(item.context.account.peerId)
}
let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let text: String
let additionalText: String
var hasActionButtons = false
if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let priceString: String
switch amount.currency {
case .stars:
priceString = "\(amount.amount) Stars"
case .ton:
priceString = "\(amount.amount) TON"
}
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let giftTitle: String
if case let .unique(gift) = gift {
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))"
uniqueGift = gift
} else {
giftTitle = ""
}
//TODO:localize
if incoming {
text = "**\(peerName)** offered you **\(priceString)** for your gift **\(giftTitle)**."
} else {
text = "You offered **\(peerName)** **\(priceString)** for their gift **\(giftTitle)**."
}
if isAccepted {
additionalText = "This offer was accepted."
} else if isDeclined {
additionalText = "This offer was rejected."
} else if expireDate > currentTimestamp {
func textForTimeout(_ value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
//let seconds = value % 60
//let secondsPadding = seconds < 10 ? "0" : ""
return "\(minutes)m" //\(secondsPadding)\(seconds)"
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
let minutesPadding = minutes < 10 ? "0" : ""
//let seconds = value % 60
//let secondsPadding = seconds < 10 ? "0" : ""
return "\(hours)h \(minutesPadding)\(minutes)m" //:\(secondsPadding)\(seconds)"
}
}
let delta = expireDate - currentTimestamp
additionalText = "This offer expires in \(textForTimeout(delta))."
if incoming {
hasActionButtons = true
}
} else {
additionalText = "This offer has expired."
}
} else {
text = ""
additionalText = ""
}
let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0
let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0)
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
if let strongSelf = self {
strongSelf.item = item
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.updateVisibility()
let _ = titleApply()
let _ = subtitleApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size)
strongSelf.subtitleNode.textNode.frame = textFrame
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
if let backgroundContent = strongSelf.mediaBackgroundContent {
animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
backgroundContent.clipsToBounds = true
if hasActionButtons {
backgroundContent.cornerRadius = 0.0
if backgroundContent.view.mask == nil {
backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil))
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil))
context.fillPath()
}))
}
} else {
backgroundContent.view.mask = nil
backgroundContent.cornerRadius = 24.0
}
}
if let uniqueGift {
let iconSize = CGSize(width: 94.0, height: 94.0)
let _ = strongSelf.giftIcon.update(
transition: .immediate,
component: AnyComponent(GiftItemComponent(
context: item.context,
theme: item.presentationData.theme.theme,
strings: item.presentationData.strings,
peer: nil,
subject: .uniqueGift(gift: uniqueGift, price: nil),
mode: .thumbnail
)),
environment: {},
containerSize: iconSize
)
if let giftIconView = strongSelf.giftIcon.view {
if giftIconView.superview == nil {
// backgroundView.layer.cornerRadius = 20.0
//backgroundView.clipsToBounds = true
strongSelf.view.addSubview(giftIconView)
}
giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize)
}
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
switch strongSelf.visibility {
case .none:
strongSelf.subtitleNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.subtitleNode.visibilityRect = subRect
}
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let mediaBackgroundContent = self.mediaBackgroundContent {
var backgroundFrame = mediaBackgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
}
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
}
override public func unreadMessageRangeUpdated() {
self.updateVisibility()
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundContent?.frame.contains(point) == true {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
private func updateVisibility() {
}
}

View file

@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/AnimatedCountLabelNode",
"//submodules/AudioWaveform",
"//submodules/DeviceProximity",

View file

@ -35,6 +35,7 @@ import TelegramStringFormatting
import AnimatedCountLabelNode
import AudioWaveform
import DeviceProximity
import ShimmeringLinkNode
private struct FetchControls {
let fetch: (Bool) -> Void
@ -123,6 +124,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
public let fetchingTextNode: ImmediateTextNode
public let fetchingCompactTextNode: ImmediateTextNode
private let countNode: ImmediateAnimatedCountLabelNode
private var shimmeringNodes: [ShimmeringLinkNode] = []
public var waveformView: ComponentHostView<Empty>?
@ -809,23 +811,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var displayTrailingAnimatedDots = false
var isTranslating = false
if let transcribedText = transcribedText, case .expanded = effectiveAudioTranscriptionState {
switch transcribedText {
case let .success(text, isPending):
textString = NSAttributedString(string: text, font: textFont, textColor: messageTheme.primaryTextColor)
/*#if DEBUG
var isPending = isPending
if "".isEmpty {
isPending = true
}
#endif*/
if isPending {
let modifiedString = NSMutableAttributedString(attributedString: textString!)
modifiedString.append(NSAttributedString(string: "...", font: textFont, textColor: .clear))
displayTrailingAnimatedDots = true
textString = modifiedString
} else {
if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty && arguments.incoming {
isTranslating = true
for attribute in arguments.message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
textString = NSAttributedString(string: attribute.text, font: textFont, textColor: messageTheme.primaryTextColor)
isTranslating = false
break
}
}
}
}
case let .error(error):
let errorTextFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 15.0 / 17.0))
@ -1538,6 +1545,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
strongSelf.updateIsTranslating(isTranslating)
}
})
})
@ -1545,6 +1554,39 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
private func updateIsTranslating(_ isTranslating: Bool) {
guard let arguments = self.arguments else {
return
}
var rects: [[CGRect]] = []
let titleRects = (self.textNode.rangeRects(in: NSRange(location: 0, length: self.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { self.textNode.view.convert($0, to: self.textClippingNode.view) }
rects.append(titleRects)
if isTranslating, !rects.isEmpty {
if self.shimmeringNodes.isEmpty {
for rects in rects {
let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
shimmeringNode.updateRects(rects)
shimmeringNode.frame = self.bounds
shimmeringNode.updateLayout(self.bounds.size)
shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.shimmeringNodes.append(shimmeringNode)
self.textClippingNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode)
}
}
} else if !self.shimmeringNodes.isEmpty {
let shimmeringNodes = self.shimmeringNodes
self.shimmeringNodes = []
for shimmeringNode in shimmeringNodes {
shimmeringNode.alpha = 0.0
shimmeringNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmeringNode] _ in
shimmeringNode?.removeFromSupernode()
})
}
}
}
private func updateStatus(animated: Bool) {
guard let resourceStatus = self.resourceStatus else {
return

View file

@ -45,6 +45,9 @@ swift_library(
"//submodules/AnimatedCountLabelNode",
"//submodules/HexColor",
"//submodules/QrCodeUI",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/SheetComponent",
],
visibility = [
"//visibility:public",

View file

@ -36,6 +36,10 @@ import SegmentedControlNode
import AnimatedCountLabelNode
import HexColor
import QrCodeUI
import ComponentFlow
import GlassBarButtonComponent
import SheetComponent
import BundleIconComponent
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@ -717,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
}
private func iconColors(theme: PresentationTheme) -> [String: UIColor] {
let accentColor = theme.actionSheet.controlAccentColor
let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor
var colors: [String: UIColor] = [:]
colors["Sunny.Path 14.Path.Stroke 1"] = accentColor
colors["Sunny.Path 15.Path.Stroke 1"] = accentColor
@ -745,18 +749,6 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f
private let defaultEmoticon = "🏠"
private func generateShadowImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -0.5), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)), cornerRadius: 16.0)
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 20, topCapHeight: 0)
}
private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
@ -768,14 +760,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
private let scrollNodeContentNode: ASDisplayNode
private let contentContainerNode: ASDisplayNode
private let topContentContainerNode: SparseNode
private let shadowNode: ASImageNode
private let effectNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let contentBackgroundView: SheetBackgroundView
private let cancelButton = ComponentView<Empty>()
private let switchThemeButton = ComponentView<Empty>()
private let titleNode: ASTextNode
private let segmentedNode: SegmentedControlNode
private let cancelButton: HighlightableButtonNode
private let switchThemeButton: HighlightTrackingButtonNode
private let cancelButtonNode: HighlightableButtonNode
private let switchThemeButtonNode: HighlightTrackingButtonNode
private let animationContainerNode: ASDisplayNode
private var animationNode: AnimationNode
private let doneButton: SolidRoundedButtonNode
@ -841,28 +833,17 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.topContentContainerNode = SparseNode()
self.topContentContainerNode.isOpaque = false
self.shadowNode = ASImageNode()
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.image = generateShadowImage()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 16.0
self.backgroundNode.cornerRadius = 38.0
self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance
self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance)
let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
let textColor = self.presentationData.theme.actionSheet.primaryTextColor
let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
self.effectNode = ASDisplayNode(viewBlock: {
return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
})
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
self.contentBackgroundView = SheetBackgroundView()
self.titleNode = ASTextNode()
let title: String
@ -878,12 +859,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.segmentedNode.isHidden = !self.contentNode.hasVideo
self.titleNode.isHidden = !self.segmentedNode.isHidden
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Close
self.cancelButton.accessibilityTraits = [.button]
self.cancelButtonNode = HighlightableButtonNode()
self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Close
self.cancelButtonNode.accessibilityTraits = [.button]
self.switchThemeButton = HighlightTrackingButtonNode()
self.switchThemeButtonNode = HighlightTrackingButtonNode()
self.animationContainerNode = ASDisplayNode()
self.animationContainerNode.isUserInteractionEnabled = false
@ -916,13 +897,10 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.scrollNodeContentNode.addSubnode(self.contentNode)
self.scrollNodeContentNode.addSubnode(self.shadowNode)
self.scrollNodeContentNode.addSubnode(self.backgroundNode)
self.scrollNodeContentNode.addSubnode(self.contentContainerNode)
self.scrollNodeContentNode.addSubnode(self.topContentContainerNode)
self.backgroundNode.addSubnode(self.effectNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.segmentedNode)
self.contentContainerNode.addSubnode(self.doneButton)
@ -930,12 +908,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.topContentContainerNode.addSubnode(self.animationContainerNode)
self.animationContainerNode.addSubnode(self.animationNode)
self.topContentContainerNode.addSubnode(self.switchThemeButton)
//self.topContentContainerNode.addSubnode(self.switchThemeButtonNode)
self.topContentContainerNode.addSubnode(self.listNode)
self.topContentContainerNode.addSubnode(self.cancelButton)
//self.topContentContainerNode.addSubnode(self.cancelButtonNode)
self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside)
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.switchThemeButtonNode.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside)
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.segmentedNode.selectedIndexChanged = { [weak self] index in
guard let strongSelf = self, let contentNode = strongSelf.contentNode as? MessageContentNode, let videoNode = contentNode.videoNode else {
@ -1181,7 +1159,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
}
}))
self.switchThemeButton.highligthedChanged = { [weak self] highlighted in
self.switchThemeButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity")
@ -1280,14 +1258,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor))
let previousIconColors = iconColors(theme: previousTheme)
let newIconColors = iconColors(theme: self.presentationData.theme)
if !self.switchThemeButton.isUserInteractionEnabled {
if !self.switchThemeButtonNode.isUserInteractionEnabled {
let themeCrossfadeDuration: Double = 0.3
let themeCrossfadeDelay: Double = 0.25
@ -1312,6 +1290,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.backgroundNode.view.addSubview(self.contentBackgroundView)
self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true
}
@ -1320,9 +1300,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
}
@objc private func switchThemePressed() {
self.switchThemeButton.isUserInteractionEnabled = false
self.switchThemeButtonNode.isUserInteractionEnabled = false
Queue.mainQueue().after(0.5) {
self.switchThemeButton.isUserInteractionEnabled = true
self.switchThemeButtonNode.isUserInteractionEnabled = true
}
self.animateCrossfade(animateIcon: false)
@ -1372,18 +1352,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
})
}
Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) {
if let effectView = self.effectNode.view as? UIVisualEffectView {
UIView.animate(withDuration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) {
effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
} completion: { _ in
}
}
let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration)
}
// Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) {
// let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
// self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
// self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration)
// }
if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.contentContainerNode.frame
@ -1403,7 +1376,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
private var animatedOut = false
public func animateIn() {
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let offset = self.bounds.size.height - self.contentBackgroundView.frame.minY
if let (layout, _) = self.containerLayout {
self.scrollNodeContentNode.cornerRadius = layout.deviceMetrics.screenCornerRadius
@ -1424,7 +1397,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.wrappingScrollNode.view.isScrollEnabled = false
let distance = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let distance = self.bounds.size.height - self.contentBackgroundView.frame.minY
if let velocity {
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity / distance)
self.wrappingScrollNode.layer.animateSpring(from: 0.0 as NSNumber, to: -distance as NSNumber, keyPath: "bounds.origin.y", duration: 0.45, delay: 0.0, initialVelocity: initialVelocity, damping: 124.0, removeOnCompletion: false, additive: true, completion: { _ in
@ -1473,24 +1446,81 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset = floor((layout.size.width - width) / 2.0)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight - 6.0), size: CGSize(width: width, height: contentHeight)).insetBy(dx: 6.0, dy: 0.0)
let contentFrame = contentContainerFrame
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
let shadowFrame = CGRect(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0, width: backgroundFrame.width, height: 40.0)
transition.updateFrame(node: self.shadowNode, frame: shadowFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
self.contentBackgroundView.update(size: contentFrame.size, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, transition: .immediate)
transition.updateFrame(view: self.contentBackgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
let barButtonSize = CGSize(width: 40.0, height: 40.0)
let cancelButtonSize = self.cancelButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: self.presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak self] _ in
self?.cancelButtonPressed()
}
)),
environment: {},
containerSize: barButtonSize
)
let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize)
if let view = self.cancelButton.view {
if view.superview == nil {
self.topContentContainerNode.view.addSubview(view)
}
view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size)
view.center = cancelButtonFrame.center
}
let switchThemeButtonSize = self.switchThemeButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: self.presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "switchTheme", component: AnyComponent(
Rectangle(color: .clear)
)),
action: { [weak self] _ in
self?.switchThemePressed()
}
)),
environment: {},
containerSize: barButtonSize
)
let switchThemeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeButtonSize.width - 16.0, y: 16.0), size: switchThemeButtonSize)
if let view = self.switchThemeButton.view {
if view.superview == nil {
self.topContentContainerNode.view.addSubview(view)
self.topContentContainerNode.view.bringSubviewToFront(self.animationContainerNode.view)
}
view.bounds = CGRect(origin: .zero, size: switchThemeButtonFrame.size)
view.center = switchThemeButtonFrame.center
}
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height + 2000.0)))
let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 36.0 - titleSize.height / 2.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let segmentedSize = self.segmentedNode.updateLayout(.sizeToFit(maximumWidth: width - 90.0, minimumWidth: 160.0, height: 32.0), transition: transition)
@ -1498,20 +1528,20 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let switchThemeSize = CGSize(width: 44.0, height: 44.0)
let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize)
transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame)
transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0))
transition.updateFrame(node: self.switchThemeButtonNode, frame: switchThemeFrame)
transition.updateFrame(node: self.animationContainerNode, frame: switchThemeButtonFrame.insetBy(dx: 5.0, dy: 5.0))
transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size))
let cancelSize = CGSize(width: 44.0, height: 44.0)
let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
let buttonInset: CGFloat = 30.0
let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: scanButtonHeight))
transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom, width: contentFrame.width, height: scanButtonHeight))
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight))
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom, width: contentFrame.width, height: doneButtonHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame)
@ -1523,7 +1553,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let contentSize = CGSize(width: contentFrame.width, height: 120.0)
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width)
self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0)
self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 12.0)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.contentNode.updateLayout(size: layout.size, topInset: 44.0, bottomInset: contentHeight, transition: transition)

View file

@ -18,6 +18,10 @@ import UIKitRuntimeUtils
public final class GiftCompositionComponent: Component {
public class ExternalState {
public fileprivate(set) var previewPatternColor: UIColor?
public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute?
public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute?
public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute?
public init() {
self.previewPatternColor = nil
}
@ -671,18 +675,23 @@ public final class GiftCompositionComponent: Component {
loop = true
self.stopSpinIfNeeded()
if self.previewModels.isEmpty {
if self.previewModels.isEmpty || sampleAttributes.count == 3 {
var models: [StarGift.UniqueGift.Attribute] = []
var patterns: [StarGift.UniqueGift.Attribute] = []
var backdrops: [StarGift.UniqueGift.Attribute] = []
for attribute in sampleAttributes {
switch attribute {
case .model: models.append(attribute)
case .model: models.append(attribute)
case .pattern: patterns.append(attribute)
case .backdrop: backdrops.append(attribute)
default: break
}
}
if self.previewModels != models && sampleAttributes.count == 3 {
self.animatePreviewTransition = true
}
self.previewModels = models
self.previewPatterns = patterns
self.previewBackdrops = backdrops
@ -707,19 +716,22 @@ public final class GiftCompositionComponent: Component {
}
if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] {
animationFile = file
component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)]
}
if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] {
patternFile = file
files[file.fileId.id] = file
component.externalState?.previewSymbol = self.previewPatterns[Int(self.previewPatternIndex)]
}
if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
component.externalState?.previewBackdrop = self.previewBackdrops[Int(self.previewBackdropIndex)]
}
}
if self.previewTimer == nil {
if self.previewTimer == nil && sampleAttributes.count > 3 {
self.previewTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in
guard let self, !self.previewModels.isEmpty else { return }
self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count)

View file

@ -30,6 +30,7 @@ public final class GiftItemComponent: Component {
case starGift(gift: StarGift.Gift, price: String)
case uniqueGift(gift: StarGift.UniqueGift, price: String?)
case auction(gift: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32)
case preview(attributes: [StarGift.UniqueGift.Attribute], rarity: Int32)
}
public struct Ribbon: Equatable {
@ -144,6 +145,7 @@ public final class GiftItemComponent: Component {
case buttonIcon
case tableIcon
case header
case upgradePreview
}
let context: AccountContext
@ -421,6 +423,14 @@ public final class GiftItemComponent: Component {
size = availableSize
iconSize = CGSize(width: 106.0, height: 106.0)
cornerRadius = 16.0
case .upgradePreview:
size = availableSize
if case let .preview(attributes, _) = component.subject, attributes.count == 2 {
iconSize = CGSize(width: 60.0, height: 60.0)
} else {
iconSize = CGSize(width: 72.0, height: 72.0)
}
cornerRadius = 16.0
}
var backgroundSize = size
if case .grid = component.mode {
@ -565,6 +575,45 @@ public final class GiftItemComponent: Component {
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
case let .preview(attributes, _):
animationOffset = 16.0
explicitAnimationOffset = -4.0
for attribute in attributes {
switch attribute {
case let .model(_, file, _):
animationFile = file
if !self.fetchedFiles.contains(file.fileId.id) {
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
self.fetchedFiles.insert(file.fileId.id)
}
case let .pattern(_, file, _):
patternFile = file
files[file.fileId.id] = file
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
if let backgroundColor {
placeholderColor = backgroundColor
}
default:
break
}
}
if animationFile == nil, let patternFile {
animationFile = patternFile
}
if let animationFile {
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: animationFile.fileId.id,
file: animationFile
)
} else {
emoji = nil
}
}
if [.buttonIcon, .tableIcon].contains(component.mode) {
@ -640,7 +689,62 @@ public final class GiftItemComponent: Component {
}
}
if case .preview = component.mode {
if case .upgradePreview = component.mode, case let .preview(attributes, rarity) = component.subject {
let isColored = attributes.count > 1
if let title = component.title {
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.medium(13.0), textColor: isColored ? .white : component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: size.height - 27.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
}
func formatPercentage(_ value: Float) -> String {
return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%"
}
let percentage = Float(rarity) * 0.1
let badgeTextSize = self.badgeText.update(
transition: .spring(duration: 0.2),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: formatPercentage(percentage), font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: isColored ? .white : component.theme.list.itemSecondaryTextColor)))
),
environment: {},
containerSize: availableSize
)
let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 12.0, height: 18.0)
let _ = self.badgeBackground.update(
transition: .spring(duration: 0.2),
component: AnyComponent(
RoundedRectangle(color: isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06), cornerRadius: 9.0)
),
environment: {},
containerSize: badgeBackgroundSize
)
if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view {
if badgeBackgroundView.superview == nil {
self.addSubview(badgeBackgroundView)
self.addSubview(badgeTextView)
}
badgeTextView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - badgeTextSize.width, y: 9.0), size: badgeTextSize)
badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize)
}
} else if case .preview = component.mode {
if let title = component.title {
let titleSize = self.title.update(
transition: transition,
@ -793,7 +897,9 @@ public final class GiftItemComponent: Component {
case .auction:
buttonColor = .clear
price = ""
break
case .preview:
buttonColor = .clear
price = ""
}
let buttonSize = self.button.update(
@ -1189,9 +1295,19 @@ public final class GiftItemComponent: Component {
}
switch component.mode {
case .generic, .grid:
case .generic, .grid, .upgradePreview:
let lineWidth: CGFloat = 2.0
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
let selectionFrame = backgroundFrame.insetBy(dx: 2.0, dy: 2.0)
var cornerRadius: CGFloat = 6.0
if case .upgradePreview = component.mode {
cornerRadius = 13.0
}
var selectionColor = UIColor.white
if case .upgradePreview = component.mode, case let .preview(attributes, _) = component.subject, attributes.count == 1 {
selectionColor = component.theme.list.itemAccentColor
}
if component.isSelected {
let selectionLayer: SimpleShapeLayer
@ -1207,13 +1323,13 @@ public final class GiftItemComponent: Component {
}
selectionLayer.fillColor = UIColor.clear.cgColor
selectionLayer.strokeColor = UIColor.white.cgColor
selectionLayer.strokeColor = selectionColor.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
if !transition.animation.isImmediate {
let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
}
@ -1222,7 +1338,7 @@ public final class GiftItemComponent: Component {
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
selectionLayer.removeFromSuperlayer()

View file

@ -61,6 +61,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/SegmentControlComponent",
],
visibility = [
"//visibility:public",

View file

@ -1141,8 +1141,7 @@ private final class GiftAuctionBidScreenComponent: Component {
private let sliderPlus = ComponentView<Empty>()
private let badge = ComponentView<Empty>()
private var liveStreamPerks: [ComponentView<Empty>] = []
private var liveStreamMessagePreview: ComponentView<Empty>?
private var auctionStats: [ComponentView<Empty>] = []
private let myGifts = ComponentView<Empty>()
@ -2366,7 +2365,7 @@ private final class GiftAuctionBidScreenComponent: Component {
self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.midX, y: badgeFrame.maxY - 32.0))
}
var perks: [([AnimatedTextComponent.Item], String)] = []
var auctionStats: [([AnimatedTextComponent.Item], String)] = []
var minBidAnimatedItems: [AnimatedTextComponent.Item] = []
var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = []
@ -2458,62 +2457,62 @@ private final class GiftAuctionBidScreenComponent: Component {
}
}
perks.append((
auctionStats.append((
minBidAnimatedItems,
environment.strings.Gift_AuctionBid_MinimumBid
))
perks.append((
auctionStats.append((
untilNextDropAnimatedItems,
environment.strings.Gift_AuctionBid_UntilNext
))
perks.append((
auctionStats.append((
dropsLeftAnimatedItems,
environment.strings.Gift_AuctionBid_Left
))
contentHeight += 54.0
let perkHeight: CGFloat = 60.0
let perkSpacing: CGFloat = 10.0
let perkWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count))
let statSpacing: CGFloat = 10.0
let statWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - statSpacing * CGFloat(auctionStats.count - 1)) / CGFloat(auctionStats.count))
let statHeight: CGFloat = 60.0
for i in 0 ..< perks.count {
var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight))
if i == perks.count - 1 {
perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX)
for i in 0 ..< auctionStats.count {
var statFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (statWidth + statSpacing), y: contentHeight), size: CGSize(width: statWidth, height: statHeight))
if i == auctionStats.count - 1 {
statFrame.size.width = max(0.0, availableSize.width - sideInset - statFrame.minX)
}
let perkView: ComponentView<Empty>
if self.liveStreamPerks.count > i {
perkView = self.liveStreamPerks[i]
let statView: ComponentView<Empty>
if self.auctionStats.count > i {
statView = self.auctionStats[i]
} else {
perkView = ComponentView()
self.liveStreamPerks.append(perkView)
statView = ComponentView()
self.auctionStats.append(statView)
}
let perk = perks[i]
let _ = perkView.update(
let stat = auctionStats[i]
let _ = statView.update(
transition: transition,
component: AnyComponent(AuctionStatComponent(
context: component.context,
gift: i == perks.count - 1 ? component.auctionContext.gift : nil,
title: perk.0,
subtitle: perk.1,
gift: i == auctionStats.count - 1 ? component.auctionContext.gift : nil,
title: stat.0,
subtitle: stat.1,
small: false,
theme: environment.theme
)),
environment: {},
containerSize: perkFrame.size
containerSize: statFrame.size
)
if let perkComponentView = perkView.view {
if let perkComponentView = statView.view {
if perkComponentView.superview == nil {
self.scrollContentView.addSubview(perkComponentView)
}
transition.setFrame(view: perkComponentView, frame: perkFrame)
transition.setFrame(view: perkComponentView, frame: statFrame)
}
}
contentHeight += perkHeight
contentHeight += statHeight
contentHeight += 24.0
let acquiredGiftsCount = self.giftAuctionState?.myState.acquiredCount ?? 0

View file

@ -71,7 +71,10 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
fileprivate var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = []
private var giftAuctionAcquiredGiftsPromise = ValuePromise<[GiftAuctionAcquiredGift]>()
private var giftAuctionAcquiredGiftsDisposable = MetaDisposable()
private(set) var giftValueInfo: StarGift.UniqueGift.ValueInfo?
private var giftValueInfoDisposable: Disposable?
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
@ -102,6 +105,17 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
if let acquiredCount = auctionState?.myState.acquiredCount, acquiredCount > (previousState?.myState.acquiredCount ?? 0) {
self.loadAcquiredGifts()
}
if !"".isEmpty, self.giftValueInfoDisposable == nil, let auctionState, case let .generic(gift) = auctionState.gift, case .finished = auctionState.auctionState, let slug = gift.auctionSlug {
self.giftValueInfoDisposable = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: "\(slug)-1")
|> deliverOnMainQueue).start(next: { [weak self] valueInfo in
guard let self else {
return
}
self.giftValueInfo = valueInfo
self.updated(transition: .easeInOut(duration: 0.25))
})
}
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
@ -114,6 +128,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
self.disposable?.dispose()
self.giftAuctionAcquiredGiftsDisposable.dispose()
self.giftAuctionTimer?.invalidate()
self.giftValueInfoDisposable?.dispose()
}
func loadAcquiredGifts() {
@ -291,6 +306,26 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
controller.present(shareController, in: .window(.root))
}
func openGiftResale() {
guard let controller = self.getController() as? GiftAuctionViewScreen, let gift = self.giftAuctionState?.gift, case let .generic(gift) = gift else {
return
}
let storeController = self.context.sharedContext.makeGiftStoreController(
context: self.context,
peerId: self.context.account.peerId,
gift: gift
)
controller.push(storeController)
}
func openGiftFragmentResale() {
guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, let url = self.giftValueInfo?.fragmentListedUrl else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
func morePressed(view: UIView, gesture: ContextGesture?) {
guard let controller = self.getController() as? GiftAuctionViewScreen else {
return
@ -367,8 +402,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
let button = Child(ButtonComponent.self)
let acquiredButton = Child(PlainButtonComponent.self)
// let telegramSaleButton = Child(PlainButtonComponent.self)
// let fragmentSaleButton = Child(PlainButtonComponent.self)
let telegramSaleButton = Child(PlainButtonComponent.self)
let fragmentSaleButton = Child(PlainButtonComponent.self)
let moreButtonPlayOnce = ActionSlot<Void>()
@ -678,8 +713,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
guard let state else {
return
}
#if DEBUG
let giftController = GiftUpgradePreviewScreen(context: component.context)
environment.controller()?.push(giftController)
#else
let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.auctionContext.gift, acquiredGifts: state.giftAuctionAcquiredGifts)
environment.controller()?.push(giftController)
#endif
}, animateScale: false),
availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
transition: context.transition
@ -692,6 +732,98 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
hasAdditionalButtons = true
}
if let giftValueInfo = state.giftValueInfo, case let .generic(gift) = component.auctionContext.gift {
if let listedCount = giftValueInfo.listedCount, listedCount > 0 {
originY += 5.0
let telegramSaleButton = telegramSaleButton.update(
component: PlainButtonComponent(content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: .starGift(gift: gift, price: ""),
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "text", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
), action: { [weak state] in
guard let state else {
return
}
state.openGiftResale()
}, animateScale: false),
availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
transition: context.transition
)
context.add(telegramSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0)))
originY += telegramSaleButton.size.height
originY += 12.0
hasAdditionalButtons = true
}
if let listedCount = giftValueInfo.fragmentListedCount, listedCount > 0 {
let fragmentSaleButton = fragmentSaleButton.update(
component: PlainButtonComponent(content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: .starGift(gift: gift, price: ""),
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "text", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
), action: { [weak state] in
guard let state else {
return
}
state.openGiftFragmentResale()
}, animateScale: false),
availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
transition: context.transition
)
context.add(fragmentSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0)))
originY += fragmentSaleButton.size.height
originY += 12.0
hasAdditionalButtons = true
}
}
if hasAdditionalButtons {
originY += 21.0
}

View file

@ -0,0 +1,485 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import GiftItemComponent
import ChatMessagePaymentAlertController
import ActivityIndicator
import TooltipUI
import MultilineTextComponent
import TelegramStringFormatting
private final class GiftOfferAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let gift: StarGift.UniqueGift
private let titleNode: ASTextNode
private let giftView = ComponentView<Empty>()
private let textNode: ASTextNode
private let arrowNode: ASImageNode
private let avatarNode: AvatarNode
private let tableView = ComponentView<Empty>()
private let modelButtonTag = GenericComponentViewTag()
private let backdropButtonTag = GenericComponentViewTag()
private let symbolButtonTag = GenericComponentViewTag()
fileprivate var getController: () -> ViewController? = { return nil}
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peer: EnginePeer,
title: String,
text: String,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.gift = gift
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
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.arrowNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
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)
}
}
fileprivate func dismissAllTooltips() {
guard let controller = self.getController() else {
return
}
controller.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
controller.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() else {
return
}
self.dismissAllTooltips()
guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 310.0)
let strings = self.strings
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
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 + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
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 + 10.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
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let tableFont = Font.regular(15.0)
let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in self.gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: self.context,
text: formatPercentage(percentage),
color: self.presentationTheme.list.itemAccentColor
)),
action: { [weak self] in
self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
if let valueAmount = self.gift.valueAmount, let valueCurrency = self.gift.valueCurrency {
tableItems.append(.init(
id: "fiatValue",
title: strings.Gift_Unique_Value,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
let tableSize = self.tableView.update(
transition: .immediate,
component: AnyComponent(
TableComponent(
theme: self.presentationTheme,
items: tableItems,
semiTransparent: true
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
)
let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize)
if let view = self.tableView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = tableFrame
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + 40.0 + 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
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
do {
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
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
do {
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
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isUserInteractionEnabled = false
actionNode.isHidden = false
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftOfferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peer: EnginePeer,
amount: CurrencyAmount,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = "Confirm Sale"
let buttonText: String = "Confirm Sale"
let priceString: String
switch amount.currency {
case .stars:
priceString = "\(amount.amount) Stars"
case .ton:
priceString = "\(amount.amount) TON"
}
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let finalPriceString: String
switch amount.currency {
case .stars:
let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0))
finalPriceString = "\(starsValue) Stars"
case .ton:
let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON"
}
let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))"
let text = "Do you want to sell **\(giftTitle)** to \(peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) for **\(priceString)**? You'll receive **\(finalPriceString)** after fees."
var contentNode: GiftOfferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
contentNode?.inProgress = true
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false)
contentNode?.getController = { [weak controller] in
return controller
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View file

@ -344,7 +344,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
id: "fiatValue",
title: strings.Gift_Unique_Value,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))

View file

@ -16,6 +16,7 @@ import ButtonComponent
import PlainButtonComponent
import GiftItemComponent
import AccountContext
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -62,8 +63,7 @@ private final class SheetContent: CombinedComponent {
}
static var body: Body {
let closeButton = Child(Button.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
@ -79,7 +79,6 @@ private final class SheetContent: CombinedComponent {
let theme = environment.theme
let strings = environment.strings
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(17.0)
@ -87,20 +86,29 @@ private final class SheetContent: CombinedComponent {
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
var contentSize = CGSize(width: context.availableSize.width, height: 10.0)
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)),
action: { [weak component] in
component?.dismiss()
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 100.0, height: 30.0),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0))
.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0))
)
let title = title.update(
@ -234,6 +242,7 @@ private final class SheetContent: CombinedComponent {
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
@ -256,7 +265,7 @@ private final class SheetContent: CombinedComponent {
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: context.transition
)
context.add(button
@ -335,6 +344,7 @@ private final class SheetContainerComponent: CombinedComponent {
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,

View file

@ -370,7 +370,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {

View file

@ -1438,7 +1438,7 @@ private final class GiftViewSheetContent: CombinedComponent {
})))
//TODO:localize
if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId {
if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId, uniqueGift.minOfferStars != nil {
items.append(.action(ContextMenuActionItem(text: "Offer to Buy", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
@ -4207,7 +4207,7 @@ private final class GiftViewSheetContent: CombinedComponent {
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))))
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
@ -5382,6 +5382,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
resellAmounts = uniqueGift.resellAmounts
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil)
case let .starGiftPurchaseOffer(gift, _, _, _, _), let .starGiftPurchaseOfferDeclined(gift, _, _):
if case let .unique(gift) = gift {
return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil)
} else {
return nil
}
default:
return nil
}

View file

@ -53,10 +53,12 @@ final class TableComponent: CombinedComponent {
private let theme: PresentationTheme
private let items: [Item]
public init(theme: PresentationTheme, items: [Item]) {
private let semiTransparent: Bool
public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) {
self.theme = theme
self.items = items
self.semiTransparent = semiTransparent
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
@ -66,6 +68,9 @@ final class TableComponent: CombinedComponent {
if lhs.items != rhs.items {
return false
}
if lhs.semiTransparent != rhs.semiTransparent {
return false
}
return true
}
@ -95,7 +100,10 @@ final class TableComponent: CombinedComponent {
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
if context.component.semiTransparent {
secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5)
}
var leftColumnWidth: CGFloat = 0.0

View file

@ -68,6 +68,8 @@ swift_library(
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
"//submodules/TelegramNotices",
"//submodules/TelegramUI/Components/AttachmentFileController",
"//submodules/SaveToCameraRoll",
],
visibility = [
"//visibility:public",

View file

@ -50,6 +50,8 @@ import UIKitRuntimeUtils
import ImageObjectSeparation
import SaveProgressScreen
import TelegramNotices
import AttachmentFileController
import SaveToCameraRoll
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@ -3732,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
} else if case let .gift(gift) = subject {
isGift = true
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false))]
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages = .single([message])
} else {
@ -5107,8 +5109,65 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
controller.push(locationController)
}
func presentAudioPicker() {
guard let controller = self.controller else {
return
}
let audioController = storyAudioPickerController(
context: self.context,
selectFromFiles: { [weak self] in
self?.presentAudioFilePicker()
},
dismissed: { [weak self] in
if let self {
self.mediaEditor?.play()
}
},
completion: { [weak self] file in
guard let self else {
return
}
let _ = (fetchMediaData(
context: self.context,
postbox: self.context.account.postbox,
userLocation: .other,
mediaReference: file
) |> deliverOnMainQueue).start(next: { [weak self] state, _ in
guard let self else {
return
}
if case let .data(data) = state {
let path = data.path
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
var originalFileName: String = "audio_\(Int.random(in: 0 ..< .max)).mp3"
if let file = file.media as? TelegramMediaFile {
originalFileName = file.fileName ?? "\(file.fileId.id).mp3"
}
let fileName = "audio_\(originalFileName)"
let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName)
try? FileManager.default.removeItem(atPath: copyPath)
do {
try FileManager.default.copyItem(atPath: path, toPath: copyPath)
} catch let e {
Logger.shared.log("MediaEditor", "copy file error \(e)")
return
}
self.insertAudio(path: copyPath, fileName: fileName)
}
})
}
)
controller.push(audioController)
}
func presentAudioFilePicker() {
var isSettingTrack = false
self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"], forceDarkTheme: true, dismissed: { [weak self] in
if let self {
@ -5119,7 +5178,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
}, completion: { [weak self] urls in
guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else {
guard let self, !urls.isEmpty, let url = urls.first else {
return
}
isSettingTrack = true
@ -5132,7 +5191,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let coordinator = NSFileCoordinator(filePresenter: nil)
var error: NSError?
coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { sourceUrl in
let fileName = "audio_\(sourceUrl.lastPathComponent)"
let fileName = "audio_\(sourceUrl.lastPathComponent)"
let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName)
try? FileManager.default.removeItem(atPath: copyPath)
@ -5147,92 +5206,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
Queue.mainQueue().async {
let audioAsset = AVURLAsset(url: URL(fileURLWithPath: copyPath))
func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) {
asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: {
if asset.statusOfValue(forKey: "tracks", error: nil) == .loading {
if retryCount < 2 {
Queue.mainQueue().after(0.1, {
loadValues(asset: asset, retryCount: retryCount + 1, completion: completion)
})
} else {
completion()
}
} else {
completion()
}
})
}
loadValues(asset: audioAsset, retryCount: 0, completion: {
var audioDuration: Double = 0.0
guard let track = audioAsset.tracks(withMediaType: .audio).first else {
Logger.shared.log("MediaEditor", "track is nil")
if isScopedResource {
url.stopAccessingSecurityScopedResource()
}
return
}
audioDuration = track.timeRange.duration.seconds
if audioDuration.isZero {
Logger.shared.log("MediaEditor", "duration is zero")
if isScopedResource {
url.stopAccessingSecurityScopedResource()
}
return
}
func maybeFixMisencodedText(_ text: String) -> String {
let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ")
if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil {
if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) {
return string
} else {
return text
}
} else {
return text
}
}
var artist: String?
var title: String?
for data in audioAsset.commonMetadata {
if data.commonKey == .commonKeyArtist, let value = data.stringValue {
artist = maybeFixMisencodedText(value)
}
if data.commonKey == .commonKeyTitle, let value = data.stringValue {
title = maybeFixMisencodedText(value)
}
}
Queue.mainQueue().async {
var audioTrimRange: Range<Double>?
var audioOffset: Double?
if let videoDuration = mediaEditor.originalCappedDuration {
if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound {
audioOffset = -videoStart
} else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound {
audioOffset = -videoStart
}
audioTrimRange = 0 ..< min(videoDuration, audioDuration)
} else {
audioTrimRange = 0 ..< min(15, audioDuration)
}
mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset)
mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true)
self.requestUpdate(transition: .easeInOut(duration: 0.2))
if isScopedResource {
url.stopAccessingSecurityScopedResource()
}
mediaEditor.play()
self.insertAudio(path: copyPath, fileName: fileName, dispose: {
if isScopedResource {
url.stopAccessingSecurityScopedResource()
}
})
}
@ -5244,6 +5220,94 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}), in: .window(.root))
}
private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) {
guard let mediaEditor = self.mediaEditor else {
return
}
let audioAsset = AVURLAsset(url: URL(fileURLWithPath: path))
func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) {
asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: {
if asset.statusOfValue(forKey: "tracks", error: nil) == .loading {
if retryCount < 2 {
Queue.mainQueue().after(0.1, {
loadValues(asset: asset, retryCount: retryCount + 1, completion: completion)
})
} else {
completion()
}
} else {
completion()
}
})
}
loadValues(asset: audioAsset, retryCount: 0, completion: {
var audioDuration: Double = 0.0
guard let track = audioAsset.tracks(withMediaType: .audio).first else {
Logger.shared.log("MediaEditor", "track is nil")
dispose?()
return
}
audioDuration = track.timeRange.duration.seconds
if audioDuration.isZero {
Logger.shared.log("MediaEditor", "duration is zero")
dispose?()
return
}
func maybeFixMisencodedText(_ text: String) -> String {
let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ")
if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil {
if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) {
return string
} else {
return text
}
} else {
return text
}
}
var artist: String?
var title: String?
for data in audioAsset.commonMetadata {
if data.commonKey == .commonKeyArtist, let value = data.stringValue {
artist = maybeFixMisencodedText(value)
}
if data.commonKey == .commonKeyTitle, let value = data.stringValue {
title = maybeFixMisencodedText(value)
}
}
Queue.mainQueue().async {
var audioTrimRange: Range<Double>?
var audioOffset: Double?
if let videoDuration = mediaEditor.originalCappedDuration {
if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound {
audioOffset = -videoStart
} else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound {
audioOffset = -videoStart
}
audioTrimRange = 0 ..< min(videoDuration, audioDuration)
} else {
audioTrimRange = 0 ..< min(15, audioDuration)
}
mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset)
mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true)
self.requestUpdate(transition: .easeInOut(duration: 0.2))
dispose?()
mediaEditor.play()
}
})
}
func presentVideoRemoveConfirmation() {
guard let controller = self.controller else {
return

View file

@ -348,7 +348,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
var contentSectionItems: [AnyComponentWithIdentity<Empty>] = []
let usdRate = Double(component.usdWithdrawRate) / 1000.0 / 100.0
let price = self.starCount == 0 ? "" : "\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
let price = self.starCount == 0 ? "" : "~\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: MessagePriceItem(

View file

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SegmentControlComponent",
module_name = "SegmentControlComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/SegmentedControlNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -7,10 +7,15 @@ import ComponentDisplayAdapters
import TelegramPresentationData
import SegmentedControlNode
final class SegmentControlComponent: Component {
struct Item: Equatable {
public final class SegmentControlComponent: Component {
public struct Item: Equatable {
var id: AnyHashable
var title: String
public init(id: AnyHashable, title: String) {
self.id = id
self.title = title
}
}
let theme: PresentationTheme
@ -18,7 +23,7 @@ final class SegmentControlComponent: Component {
let selectedId: AnyHashable?
let action: (AnyHashable) -> Void
init(
public init(
theme: PresentationTheme,
items: [Item],
selectedId: AnyHashable?,
@ -30,7 +35,7 @@ final class SegmentControlComponent: Component {
self.action = action
}
static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool {
public static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@ -80,7 +85,7 @@ final class SegmentControlComponent: Component {
}
}
class View: UIView {
public class View: UIView {
private let title = ComponentView<Empty>()
private var component: SegmentControlComponent?
@ -88,7 +93,7 @@ final class SegmentControlComponent: Component {
private var nativeSegmentedView: SegmentedControlView?
private var legacySegmentedNode: SegmentedControlNode?
override init(frame: CGRect) {
public override init(frame: CGRect) {
super.init(frame: frame)
}
@ -171,11 +176,11 @@ final class SegmentControlComponent: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
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

@ -220,7 +220,7 @@ final class StarsBalanceComponent: Component {
let subtitleText: String
if let rate = component.rate {
subtitleText = "\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))"
subtitleText = "~\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))"
} else {
subtitleText = component.strings.Stars_Intro_YourBalance
}

View file

@ -114,7 +114,7 @@ final class StarsOverviewItemComponent: Component {
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
text: .plain(NSAttributedString(string: "~\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)
),
environment: {},

View file

@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",

View file

@ -27,6 +27,7 @@ import TabSelectorComponent
import PresentationDataUtils
import BalanceNeededScreen
import GlassBarButtonComponent
import GlassBackgroundComponent
private let amountTag = GenericComponentViewTag()
@ -68,6 +69,7 @@ private final class SheetContent: CombinedComponent {
let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self)
let balanceIcon = Child(BundleIconComponent.self)
let durationPicker = Child(MenuComponent.self)
let body: (CombinedComponentContext<SheetContent>) -> CGSize = { (context: CombinedComponentContext<SheetContent>) -> CGSize in
let environment = context.environment[EnvironmentType.self]
@ -356,6 +358,7 @@ private final class SheetContent: CombinedComponent {
tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon
}
case .starGiftOffer:
//TODO:localize
displayCurrencySelector = true
starsTitle = "Offer Stars"
tonTitle = "Offer TON"
@ -625,6 +628,7 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0)))
}
var durationFrame = CGRect()
if case .starGiftResell = component.mode {
contentSize.height += 24.0
@ -808,6 +812,14 @@ private final class SheetContent: CombinedComponent {
maximumNumberOfLines: 0
))
let hours = state.duration / 3600
let durationString: String
if hours == 1 {
durationString = "1 Hour"
} else {
durationString = "\(hours) Hours"
}
let periodSection = periodSection.update(
component: ListSectionComponent(
theme: theme,
@ -831,15 +843,19 @@ private final class SheetContent: CombinedComponent {
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "48 Hours",
string: durationString,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
)))),
accessory: .expandArrows,
action: { _ in
action: { [weak state] _ in
guard let state else {
return
}
state.isPickingDuration = true
state.updated(transition: .easeInOut(duration: 0.25))
}
))
)]
@ -853,6 +869,8 @@ private final class SheetContent: CombinedComponent {
.clipsToBounds(true)
.cornerRadius(10.0)
)
durationFrame = CGRect(origin: CGPoint(x: context.availableSize.width / 2.0 - periodSection.size.width / 2.0, y: contentSize.height), size: periodSection.size)
contentSize.height += periodSection.size.height
}
@ -1065,6 +1083,58 @@ private final class SheetContent: CombinedComponent {
contentSize.height += buttonInsets.bottom
}
if state.isPickingDuration {
let durationPicker = durationPicker.update(
component: MenuComponent(
theme: theme,
sourceFrame: durationFrame.offsetBy(dx: 0.0, dy: 120.0),
component: AnyComponent(DurationMenuComponent(
theme: theme,
strings: environment.strings,
value: state.duration,
valueUpdated: { [weak state] value in
guard let state else {
return
}
state.isPickingDuration = false
state.duration = value
state.updated(transition: .easeInOut(duration: 0.25))
}
)),
dismiss: { [weak state] in
guard let state else {
return
}
state.isPickingDuration = false
state.updated(transition: .easeInOut(duration: 0.25))
}
),
availableSize: contentSize,
transition: context.transition
)
context.add(durationPicker
.position(CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0))
.appear(ComponentTransition.Appear({ _, view, transition in
if !transition.animation.isImmediate {
if let view = view as? MenuComponent.View {
view.animateIn()
}
}
}))
.disappear(ComponentTransition.Disappear({ view, transition, completion in
if !transition.animation.isImmediate {
if let view = view as? MenuComponent.View {
view.animateOut(completion: completion)
}
} else {
completion()
}
}))
)
}
return contentSize
}
@ -1080,7 +1150,7 @@ private final class SheetContent: CombinedComponent {
fileprivate var amount: StarsAmount?
fileprivate var currency: CurrencyAmount.Currency = .stars
fileprivate var timestamp: Int32?
fileprivate var duration: Int32 = 120
fileprivate var duration: Int32 = 172800
fileprivate var starsBalance: StarsAmount?
private var starsStateDisposable: Disposable?
@ -1091,6 +1161,8 @@ private final class SheetContent: CombinedComponent {
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var isPickingDuration = false
init(component: SheetContent) {
self.context = component.context
self.mode = component.mode
@ -2251,3 +2323,423 @@ private final class CurrencyTabItemComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class MenuComponent: Component {
let theme: PresentationTheme
let sourceFrame: CGRect
let component: AnyComponent<Empty>
let dismiss: () -> Void
init(
theme: PresentationTheme,
sourceFrame: CGRect,
component: AnyComponent<Empty>,
dismiss: @escaping () -> Void
) {
self.theme = theme
self.sourceFrame = sourceFrame
self.component = component
self.dismiss = dismiss
}
public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.sourceFrame != rhs.sourceFrame {
return false
}
if lhs.component != rhs.component {
return false
}
return true
}
public final class View: UIView {
private let buttonView: UIButton
private let containerView: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private var componentView: ComponentView<Empty>?
private var component: MenuComponent?
public override init(frame: CGRect) {
self.buttonView = UIButton()
self.containerView = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
super.init(frame: frame)
self.addSubview(self.buttonView)
self.addSubview(self.containerView)
self.containerView.contentView.addSubview(self.backgroundView)
self.buttonView.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func tapped() {
if let component = self.component {
component.dismiss()
}
}
func animateIn() {
guard let component = self.component else {
return
}
let transition = ComponentTransition.spring(duration: 0.3)
transition.animatePosition(view: self.backgroundView, from: component.sourceFrame.center, to: self.backgroundView.center)
transition.animateScale(view: self.backgroundView, from: 0.2, to: 1.0)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
public func animateOut(completion: (() -> Void)? = nil) {
guard let component = self.component else {
return
}
let transition = ComponentTransition.spring(duration: 0.3)
transition.setPosition(view: self.backgroundView, position: component.sourceFrame.center)
transition.setScale(view: self.backgroundView, scale: 0.2)
self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion?()
})
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.backgroundView.frame.contains(point) && self.buttonView.frame.contains(point) {
return self.buttonView
}
return super.hitTest(point, with: event)
}
func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
var componentView: ComponentView<Empty>
var componentTransition = transition
if let current = self.componentView {
componentView = current
} else {
componentTransition = .immediate
componentView = ComponentView()
self.componentView = componentView
}
let componentSize = componentView.update(
transition: componentTransition,
component: component.component,
environment: {},
containerSize: availableSize
)
let backgroundFrame = CGRect(origin: CGPoint(x: component.sourceFrame.maxX - componentSize.width, y: component.sourceFrame.minY - componentSize.height - 20.0), size: componentSize)
if let view = componentView.view {
if view.superview == nil {
self.backgroundView.contentView.addSubview(view)
}
componentTransition.setFrame(view: view, frame: CGRect(origin: .zero, size: componentSize))
}
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition)
self.backgroundView.frame = backgroundFrame
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition)
self.buttonView.frame = CGRect(origin: .zero, size: availableSize)
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)
}
}
private final class MenuButtonComponent: Component {
let theme: PresentationTheme
let text: String
let isSelected: Bool
let width: CGFloat?
let action: () -> Void
init(
theme: PresentationTheme,
text: String,
isSelected: Bool,
width: CGFloat?,
action: @escaping () -> Void
) {
self.theme = theme
self.text = text
self.isSelected = isSelected
self.width = width
self.action = action
}
static func ==(lhs: MenuButtonComponent, rhs: MenuButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.width != rhs.width {
return false
}
return true
}
final class View: UIView {
private var component: MenuButtonComponent?
private weak var componentState: EmptyComponentState?
private let selectionLayer = SimpleLayer()
private let title = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private let button = HighlightTrackingButton()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.selectionLayer)
self.selectionLayer.masksToBounds = true
self.selectionLayer.opacity = 0.0
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.button.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.selectionLayer.opacity = 1.0
self.selectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
self.selectionLayer.opacity = 0.0
self.selectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
if let component = self.component {
component.action()
}
}
func update(component: MenuButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let leftInset: CGFloat = 60.0
let rightInset: CGFloat = 40.0
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
Text(text: component.text, font: Font.regular(17.0), color: component.theme.contextMenu.primaryColor)
),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: 60.0, y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
let size = CGSize(width: component.width ?? (leftInset + rightInset + titleSize.width), height: availableSize.height)
if component.isSelected {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(
BundleIconComponent(
name: "Media Gallery/Check",
tintColor: component.theme.contextMenu.primaryColor
)
),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let iconFrame = CGRect(origin: CGPoint(x: 25.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
}
self.selectionLayer.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor.withMultipliedAlpha(0.5).cgColor
transition.setFrame(layer: self.selectionLayer, frame: CGRect(origin: .zero, size: size).insetBy(dx: 10.0, dy: 0.0))
self.selectionLayer.cornerRadius = size.height / 2.0
if self.button.superview == nil {
self.addSubview(self.button)
}
self.button.frame = CGRect(origin: .zero, size: size)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
private final class DurationMenuComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let valueUpdated: (Int32) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
value: Int32,
valueUpdated: @escaping (Int32) -> Void
) {
self.theme = theme
self.strings = strings
self.value = value
self.valueUpdated = valueUpdated
}
public static func ==(lhs: DurationMenuComponent, rhs: DurationMenuComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: GlassBackgroundView
private var itemViews: [Int32: ComponentView<Empty>] = [:]
private var component: DurationMenuComponent?
private let values: [Int32] = [
21600, 43200, 86400, 129600, 172800, 259200
]
private var width: CGFloat?
public override init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: DurationMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let itemHeight: CGFloat = 40.0
var maxWidth: CGFloat = 0.0
var originY: CGFloat = 12.0
for value in self.values {
let itemView: ComponentView<Empty>
if let current = self.itemViews[value] {
itemView = current
} else {
itemView = ComponentView()
self.itemViews[value] = itemView
}
let repeatString: String
let hours = value / 3600
//TODO:localize
if hours == 1 {
repeatString = "1 hour"
} else {
repeatString = "\(hours) hours"
}
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
MenuButtonComponent(
theme: component.theme,
text: repeatString,
isSelected: component.value == value,
width: self.width,
action: { [weak self] in
guard let self else {
return
}
self.component?.valueUpdated(value)
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: itemHeight)
)
maxWidth = max(maxWidth, itemSize.width)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: originY), size: itemSize)
if let itemView = itemView.view {
if itemView.superview == nil {
self.addSubview(itemView)
}
transition.setFrame(view: itemView, frame: itemFrame)
}
originY += 40.0
}
let size = CGSize(width: maxWidth, height: originY + 8.0)
if self.width == nil {
self.width = maxWidth
Queue.mainQueue().justDispatch {
state.updated()
}
}
return size
}
}
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

@ -29,6 +29,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/TelegramUI/Components/SliderComponent",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/CheckNode",
"//submodules/Markdown",
"//submodules/ContextUI",

View file

@ -23,6 +23,7 @@ import TelegramStringFormatting
import GalleryData
import AnimatedTextComponent
import TelegramUIPreferences
import SegmentControlComponent
final class DataUsageScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment

View file

@ -1328,6 +1328,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .paidMessagesPriceEdited:
self.interfaceInteraction?.openMonoforum()
return true
case .starGiftPurchaseOffer:
let controller = self.context.sharedContext.makeGiftViewScreen(context: self.context, message: EngineMessage(message), shareStory: { [weak self] uniqueGift in
Queue.mainQueue().after(0.15) {
if let self {
let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self)
self.push(controller)
}
}
})
self.push(controller)
return true
default:
break
}
@ -2401,7 +2412,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute {
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, _, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined {
guard let data = data?.makeData(), message.effectivelyIncoming(strongSelf.context.account.peerId) else {
return
}
if data.count != 1 {
return
}
let buttonType = data.withUnsafeBytes { buffer -> UInt8 in
return buffer.baseAddress!.assumingMemoryBound(to: UInt8.self).pointee
}
switch buttonType {
case 0:
let _ = strongSelf.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: false).startStandalone()
case 1:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer, case let .unique(gift) = gift else {
return
}
let controller = self.context.sharedContext.makeGiftOfferScreen(
context: self.context,
gift: gift,
peer: peer,
amount: amount,
commit: { [weak self] in
if let self {
let _ = self.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: true).startStandalone()
}
}
)
self.present(controller, in: .window(.root))
})
default:
break
}
return
} else if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute {
guard let data = data?.makeData() else {
return
}

View file

@ -939,13 +939,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
}
self.translationProcessingManager.process = { [weak self, weak context] messageIds in
if let context = context, let translationLang = self?.translationLang {
if let context, let translationLang = self?.translationLang {
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone()
}
}
self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in
if let context = context, let translationLang = self?.translationLang {
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone()
self.factCheckProcessingManager.process = { [weak context] messageIds in
if let context {
let _ = context.engine.messages.getMessagesFactCheck(messageIds: Array(messageIds.map(\.messageId))).startStandalone()
}
}
@ -2746,6 +2746,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
messageIdsToTranslate.append(message.id)
} else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) {
messageIdsToTranslate.append(message.id)
} else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
messageIdsToTranslate.append(message.id)
}
case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _, _) in messages {

View file

@ -3860,6 +3860,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftAuctionActiveBidsScreen(context: context)
}
public func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController {
return giftOfferAlertController(context: context, gift: gift, peer: peer, amount: amount, commit: commit)
}
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {

View file

@ -172,6 +172,11 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess
messageIdsToTranslate.append(messageId)
messageIdsSet.insert(messageId)
}
} else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
if !messageIdsSet.contains(messageId) {
messageIdsToTranslate.append(messageId)
messageIdsSet.insert(messageId)
}
}
} else {
if !messageIdsSet.contains(messageId) {

View file

@ -17,6 +17,7 @@ import AccountContext
import PresentationDataUtils
import PremiumPeerShortcutComponent
import GiftAnimationComponent
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -58,7 +59,6 @@ private final class SheetContent: CombinedComponent {
}
final class State: ComponentState {
var cachedCloseImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
@ -68,7 +68,7 @@ private final class SheetContent: CombinedComponent {
static var body: Body {
let background = Child(RoundedRectangle.self)
let animation = Child(GiftAnimationComponent.self)
let closeButton = Child(Button.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(Text.self)
let text = Child(BalancedTextComponent.self)
@ -78,7 +78,6 @@ private final class SheetContent: CombinedComponent {
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = presentationData.theme
@ -108,25 +107,27 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0))
)
let closeImage: UIImage
if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: {
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0))
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
@ -200,10 +201,10 @@ private final class SheetContent: CombinedComponent {
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
@ -216,7 +217,7 @@ private final class SheetContent: CombinedComponent {
controller?.dismissAnimated()
}
),
availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: .immediate
)
context.add(button
@ -298,6 +299,7 @@ private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent {
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,