Various improvements

This commit is contained in:
Ilya Laktyushin 2025-12-03 17:59:09 +04:00
parent 0044bde566
commit 6a09d2e4e6
17 changed files with 1014 additions and 266 deletions

View file

@ -15507,7 +15507,9 @@ Error: %8$@";
"ChatList.Auctions.UpcomingAuction" = "Upcoming Auction";
"Gift.AuctionBid.UpcomingBid" = "You placed an early bid";
"Gift.AuctionBid.UpcomingBid" = "You've placed an early bid";
"ChatList.Auctions.Status.UpcomingBid" = "You've placed an early bid.";
"Gift.ActiveAuctions.UpcomingBid" = "You've placed an early bid";
"Login.PhoneWithPasskeySubtitle" = "Enter your phone number\nor [log in using Passkey >](passkey)";
"PrivacySettings.Passkey" = "Passkey";
@ -15536,3 +15538,13 @@ Error: %8$@";
"Passkeys.PasskeyCreatedAndUsedPattern" = "created %1$@ • used %2$@";
"Passkeys.AddPasskey" = "Create Passkey";
"Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager.";
"Gifts.Demo.Title" = "Unique Collectibles";
"Gifts.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile.";
"Gifts.Demo.Unique.Title" = "Unique";
"Gifts.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol.";
"Gifts.Demo.Tradable.Title" = "Tradable";
"Gifts.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces.";
"Gifts.Demo.Wearable.Title" = "Wearable";
"Gifts.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses.";
"Gifts.Demo.Understood" = "Understood";

View file

@ -1273,6 +1273,7 @@ public protocol SharedAccountContext: AnyObject {
func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?)
func openChatMessage(_ params: OpenChatMessageParams) -> Bool
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError>
func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController?
func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
@ -1356,54 +1357,34 @@ public protocol SharedAccountContext: AnyObject {
func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController?
func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController
func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void)
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal<Never, TransferStarGiftError>)?) -> ViewController
func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController
func makeCameraScreen(context: AccountContext, mode: CameraScreenMode, cameraHolder: Any?, transitionIn: CameraScreenTransitionIn?, transitionOut: @escaping (Bool) -> CameraScreenTransitionOut?, completion: @escaping (Any, @escaping () -> Void) -> Void, transitionedOut: (() -> Void)?) -> ViewController
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, remainingCount: Int32, completion: @escaping ([MediaEditorScreenResult], MediaEditorTransitionOutExternalState, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController
func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController
func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, messageId: EngineMessage.Id) -> ViewController
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
@ -1431,33 +1412,23 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController
func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController
func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController
func makeGiftDemoScreen(context: AccountContext) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController
func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?)
func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?)
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController
func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController
func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController
func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController
func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController
@available(iOS 13.0, *)
func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController

View file

@ -354,15 +354,15 @@ protocol PhoneDemoDecorationView: UIView {
func resetAnimation()
}
final class PhoneDemoComponent: Component {
typealias EnvironmentType = DemoPageEnvironment
public final class PhoneDemoComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
enum Position {
public enum Position {
case top
case bottom
}
enum BackgroundDecoration {
public enum BackgroundDecoration {
case none
case dataRain
case swirlStars
@ -375,7 +375,7 @@ final class PhoneDemoComponent: Component {
case todo
}
enum Model {
public enum Model {
case notch
case island
}
@ -419,11 +419,11 @@ final class PhoneDemoComponent: Component {
return true
}
final class View: UIView, ComponentTaggedView {
final class Tag {
public final class View: UIView, ComponentTaggedView {
public final class Tag {
}
func matches(tag: Any) -> Bool {
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag, self.isCentral {
return true
}

View file

@ -155,7 +155,7 @@ public final class PremiumGradientBackgroundComponent: Component {
}
}
final class DemoPageEnvironment: Equatable {
public final class DemoPageEnvironment: Equatable {
public let isDisplaying: Bool
public let isCentral: Bool
public let position: CGFloat
@ -1134,13 +1134,13 @@ private final class DemoSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .glass,
backgroundColor: UIColor(rgb: 0x7f76f4),
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: .white
)
)),
action: { _ in

View file

@ -114,8 +114,21 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
if self.states.count == 1, let auctionState = self.states.first {
var isUpcoming = false
var startTime = currentTime
var endTime = currentTime
if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState {
startTime = startDate
endTime = nextRoundDate
if currentTime < startDate {
isUpcoming = true
}
}
let place = auctionState.place ?? 1
if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
if isUpcoming {
subtitleText = self.strings.ChatList_Auctions_Status_UpcomingBid
} else if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = self.strings.ChatList_Auctions_Status_Single_Outbid
subtitleTextColor = self.theme.list.itemDestructiveColor
isOutbid = true
@ -135,13 +148,6 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string
}
var startTime = currentTime
var endTime = currentTime
if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState {
startTime = startDate
endTime = nextRoundDate
}
let endTimeout: Int32
if currentTime < startTime {
endTimeout = max(0, startTime - currentTime)

View file

@ -499,6 +499,7 @@ swift_library(
"//submodules/TelegramUI/Components/Contacts/NewContactScreen",
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
"//submodules/TelegramUI/Components/Settings/PasskeysScreen",
"//submodules/TelegramUI/Components/Gifts/GiftDemoScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View file

@ -0,0 +1,42 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftDemoScreen",
module_name = "GiftDemoScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/PremiumUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,584 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import GlassBarButtonComponent
import PremiumUI
import ScrollComponent
import LottieComponent
import EdgeEffect
import InfoParagraphComponent
final class PageComponent<ChildEnvironment: Equatable>: CombinedComponent {
typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let title: String
private let text: String
private let textColor: UIColor
init(
content: AnyComponent<ChildEnvironment>,
title: String,
text: String,
textColor: UIColor
) {
self.content = content
self.title = title
self.text = text
self.textColor = textColor
}
static func ==(lhs: PageComponent<ChildEnvironment>, rhs: PageComponent<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
return true
}
static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
return { context in
let availableSize = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = 24.0
let textColor = component.textColor
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let content = children["main"].update(
component: component.content,
environment: {
context.environment[ChildEnvironment.self]
},
availableSize: CGSize(width: availableSize.width, height: availableSize.width),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: boldTextFont,
textColor: component.textColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0))
)
context.add(content
.position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
)
return availableSize
}
}
}
private final class DemoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let action: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
private(set) var promoConfiguration: PremiumPromoConfiguration?
fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.PremiumPromo())
|> deliverOnMainQueue).start(next: { [weak self] promoConfiguration in
guard let self else {
return
}
self.promoConfiguration = promoConfiguration
self.updated(transition: .immediate)
})
}
deinit {
self.disposable?.dispose()
}
func playAnimationIfNeeded() {
if !self.didPlayAnimation {
self.didPlayAnimation = true
self.playButtonAnimation.invoke(Void())
}
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let background = Child(PremiumGradientBackgroundComponent.self)
let demo = Child(PhoneDemoComponent.self)
let scroll = Child(ScrollComponent<Empty>.self)
let bottomEdgeEffect = Child(EdgeEffectComponent.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
var contentSize = CGSize(width: context.availableSize.width, height: context.availableSize.width)
let remainingHeight = 365.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
GiftDemoListComponent(
context: context.component.context,
theme: environment.theme
)
),
contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 80.0, right: 0.0),
contentOffsetUpdated: { _, _ in },
contentOffsetWillCommit: { _ in }
),
availableSize: CGSize(width: context.availableSize.width, height: remainingHeight),
transition: context.transition
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + scroll.size.height / 2.0))
)
let background = background.update(
component: PremiumGradientBackgroundComponent(colors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let demo = demo.update(
component: PhoneDemoComponent(
context: component.context,
position: .top,
model: .island,
videoFile: state.promoConfiguration?.videos["gifts"],
decoration: .badgeStars
),
environment: { DemoPageEnvironment(isDisplaying: true, isCentral: true, position: 0.0) },
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: context.transition
)
context.add(demo
.position(CGPoint(x: context.availableSize.width / 2.0, y: demo.size.height / 2.0))
)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: UIColor(rgb: 0x7f76f4),
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: .white
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
contentSize.height += remainingHeight
let bottomEdgeEffectHeight = 108.0
let bottomEdgeEffect = bottomEdgeEffect.update(
component: EdgeEffectComponent(
color: .clear,
blur: true,
alpha: 1.0,
size: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
edge: .bottom,
edgeSize: bottomEdgeEffectHeight
),
availableSize: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
transition: context.transition
)
context.add(bottomEdgeEffect
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - bottomEdgeEffect.size.height / 2.0))
)
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: state.playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: strings.Gifts_Demo_Understood,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))))
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),
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
),
action: {
component.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - button.size.height / 2.0 - 30.0))
)
state.playAnimationIfNeeded()
return contentSize
}
}
}
private final class DemoSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let action: () -> Void
init(context: AccountContext, action: @escaping () -> Void) {
self.context = context
self.action = action
}
static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(DemoSheetContent(
context: context.component.context,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: nil,
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class GiftDemoScreen: ViewControllerComponentContainer {
public init(context: AccountContext, action: @escaping () -> Void = {}) {
super.init(context: context, component: DemoSheetComponent(context: context, action: action), navigationBarAppearance: .none, theme: .default)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class GiftDemoListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
init(context: AccountContext, theme: PresentationTheme) {
self.context = context
self.theme = theme
}
static func ==(lhs: GiftDemoListComponent, rhs: GiftDemoListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let description = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
return { context in
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let titleColor = theme.list.itemPrimaryTextColor
let textColor = theme.list.itemSecondaryTextColor
let iconColor = theme.list.itemAccentColor
var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
let title = title.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gifts_Demo_Title, font: Font.bold(25.0), textColor: titleColor))),
availableSize: context.availableSize,
transition: context.transition
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height * 0.5))
)
contentSize.height += title.size.height
contentSize.height += 9.0
let description = description.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gifts_Demo_Description, font: Font.regular(15.0), textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5))
)
contentSize.height += description.size.height
contentSize.height += 26.0
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "unique",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gifts_Demo_Unique_Title,
titleColor: titleColor,
text: strings.Gifts_Demo_Unique_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Unique",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "tradable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gifts_Demo_Tradable_Title,
titleColor: titleColor,
text: strings.Gifts_Demo_Tradable_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Transferable",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "wearable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gifts_Demo_Wearable_Title,
titleColor: titleColor,
text: strings.Gifts_Demo_Wearable_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Tradable",
iconColor: iconColor
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - 32.0 * 2.0, height: 10000.0),
transition: context.transition
)
context.add(list
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
)
contentSize.height += list.size.height
contentSize.height += 88.0
return CGSize(width: context.availableSize.width, height: contentSize.height)
}
}
}

View file

@ -63,6 +63,7 @@ swift_library(
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
"//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",

View file

@ -663,11 +663,13 @@ private final class ActiveAuctionComponent: Component {
var titleText: String = ""
var subtitleText: String = ""
var subtitleTextColor = component.theme.list.itemPrimaryTextColor
if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string
let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))"
if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
if component.currentTime < startDate {
subtitleText = component.strings.Gift_ActiveAuctions_UpcomingBid
} else if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string
subtitleTextColor = component.theme.list.itemDestructiveColor
} else {

View file

@ -1870,7 +1870,7 @@ private final class GiftAuctionBidScreenComponent: Component {
maxBidAmount = max(maxBidAmount, Int64(Double(firstLevel.amount) * 1.5))
}
}
var currentValue = max(Int(minBidAmount), 100)
var currentValue = Int(minBidAmount)
var minAllowedRealValue: Int64 = minBidAmount
if let myBidAmount = state.myState.bidAmount {
if let component, let bidPeerId = state.myState.bidPeerId, bidPeerId != component.toPeerId || forceMinimum, let myMinBidAmount = state.myState.minBidAmount {

View file

@ -818,10 +818,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
tintColor: secondaryTextColor
)),
action: {
let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(
context: component.context,
auctionContext: component.auctionContext,
completion: nil
let controller = component.context.sharedContext.makeGiftDemoScreen(
context: component.context
)
environment.controller()?.push(controller)
},
@ -1157,7 +1155,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("buy"),
id: AnyHashable("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,

View file

@ -25,6 +25,7 @@ import GiftItemComponent
import GiftAnimationComponent
import GlassBarButtonComponent
import GiftRemainingCountComponent
import AnimatedTextComponent
private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -63,7 +64,11 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
private var disposable: Disposable?
private(set) var giftAuctionState: GiftAuctionContext.State?
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var previewTimer: SwiftSignalKit.Timer?
private(set) var previewModelIndex: Int = 0
private(set) var previewBackdropIndex: Int = 0
@ -75,13 +80,13 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute]
) {
self.context = context
super.init()
for attribute in attributes {
switch attribute {
case .model:
@ -123,9 +128,25 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
self.previewTimerTick()
}, queue: Queue.mainQueue())
self.previewTimer?.start()
self.disposable = (auctionContext.state
|> deliverOnMainQueue).start(next: { [weak self] auctionState in
guard let self else {
return
}
self.giftAuctionState = auctionState
self.updated()
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
deinit {
self.disposable?.dispose()
self.giftAuctionTimer?.invalidate()
self.peerDisposable?.dispose()
self.previewTimer?.invalidate()
}
@ -154,7 +175,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
func makeState() -> State {
return State(context: self.context, attributes: self.attributes)
return State(context: self.context, auctionContext: self.auctionContext, attributes: self.attributes)
}
static var body: Body {
@ -408,17 +429,18 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
contentHeight += 137.0
if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability {
let remains: Int32 = availability.remains
// if let auctionState = component.auctionContext.currentState {
// switch auctionState.auctionState {
// case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
// remains = giftsLeft
// case .finished:
// remains = 0
// }
// }
let total: Int32 = availability.total
var remains: Int32 = availability.remains
if let auctionState = state.giftAuctionState {
switch auctionState.auctionState {
case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
remains = giftsLeft
case .finished:
remains = 0
}
}
let total = availability.total
let position = CGFloat(remains) / CGFloat(total)
let sold = total - remains
let remainingCount = remainingCount.update(
component: GiftRemainingCountComponent(
inactiveColor: theme.list.itemBlocksBackgroundColor,
@ -427,7 +449,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
inactiveValue: "",
inactiveTitleColor: theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: "",
activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "",
activeTitleColor: .white,
badgeText: "",
badgePosition: position,
@ -494,11 +516,77 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
}
contentHeight += 80.0
var buttonTitle = strings.Gift_Auction_Join
if component.auctionContext.isUpcoming {
buttonTitle = strings.Gift_Auction_EarlyBid
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var startTime = currentTime
var endTime = currentTime
var isUpcoming = false
if let auctionState = state.giftAuctionState {
startTime = auctionState.startDate
endTime = auctionState.endDate
}
var buttonTitle = strings.Gift_Auction_Join
let endTimeout: Int32
if currentTime < startTime {
isUpcoming = true
endTimeout = max(0, startTime - currentTime)
} else {
endTimeout = max(0, endTime - currentTime)
}
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
let rawString: String
if isUpcoming {
buttonTitle = strings.Gift_Auction_EarlyBid
rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes
} else {
rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
}
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
if range.lowerBound != startIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
}
startIndex = range.upperBound
if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
let controlString = rawString[range.upperBound ..< endRange.lowerBound]
if controlString == "h" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2)))
} else if controlString == "m" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
} else if controlString == "s" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
}
startIndex = endRange.upperBound
}
} else {
break
}
}
if startIndex != rawString.endIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
}
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let items: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers),
color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)))
]
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
let buttonBackground = ButtonComponent.Background(
@ -511,8 +599,8 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("ok"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
id: AnyHashable("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,
displaysProgress: false,
@ -523,7 +611,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
}
}),
availableSize: buttonSize,
transition: context.transition
transition: .spring(duration: 0.2)
)
let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size)

View file

@ -39,6 +39,7 @@ import GiftAnimationComponent
import ChatThemeScreen
import ProfileLevelRatingBarComponent
import AnimatedTextComponent
import InfoParagraphComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -2885,7 +2886,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "badge",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Badge_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Badge_ChannelText : strings.Gift_Wear_Badge_Text,
@ -2899,7 +2900,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "design",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Design_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Design_ChannelText : strings.Gift_Wear_Design_Text,
@ -2913,7 +2914,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "proof",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Proof_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Proof_ChannelText : strings.Gift_Wear_Proof_Text,
@ -3031,7 +3032,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "unique",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Unique_Title,
titleColor: textColor,
text: uniqueText,
@ -3045,7 +3046,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "transferable",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Transferable_Title,
titleColor: textColor,
text: transferableText,
@ -3059,7 +3060,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "tradable",
component: AnyComponent(ParagraphComponent(
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Tradable_Title,
titleColor: textColor,
text: tradableText,
@ -6046,184 +6047,6 @@ private struct GiftConfiguration {
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let badge: String?
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
badge: String? = nil,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.badge = badge
self.action = action
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0),
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
context.add(badgeText
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
}
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}
private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ASDisplayNode

View file

@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InfoParagraphComponent",
module_name = "InfoParagraphComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Markdown",
"//submodules/TextFormat",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,187 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import Markdown
import TextFormat
public final class InfoParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let badge: String?
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
badge: String? = nil,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.badge = badge
self.action = action
}
public static func ==(lhs: InfoParagraphComponent, rhs: InfoParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
public static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0),
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
context.add(badgeText
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
}
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}

View file

@ -91,6 +91,7 @@ import GlassBackgroundComponent
import AttachmentFileController
import NewContactScreen
import PasskeysScreen
import GiftDemoScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -3876,6 +3877,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, attributes: attributes, completion: completion)
}
public func makeGiftDemoScreen(context: AccountContext) -> ViewController {
return GiftDemoScreen(context: context)
}
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {