Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Isaac 2025-09-01 18:44:15 +02:00
commit 980ced37a8
59 changed files with 2624 additions and 911 deletions

View File

@ -14924,3 +14924,87 @@ Sorry for the inconvenience.";
"Gift.Upgrade.GiftUpgrade" = "Pay %@ for Upgrade"; "Gift.Upgrade.GiftUpgrade" = "Pay %@ for Upgrade";
"Gift.View.GiftUpgrade" = "Gift an Upgrade"; "Gift.View.GiftUpgrade" = "Gift an Upgrade";
"Gift.View.OpenChatTheme" = "This gift is the chat's theme. [Change Theme >]()";
"Notification.ChatTheme.Text" = "%1$@ set **%2$@** as a new theme for this chat.";
"Notification.ChatTheme.TextYou" = "You set **%@** as a new theme for this chat.";
"Notification.ChangedThemeGift" = "%1$@ changed chat theme to %2$@";
"Notification.YouChangedThemeGift" = "You changed chat theme to %@";
"Conversation.Theme.GiftTransfer.Text" = "This gift is already your theme in the chat with **%@**. Remove it there and use it here instead?";
"Conversation.Theme.GiftTransfer.Proceed" = "Yes";
"PeerInfo.Tabs.SetMainTab" = "Set as Main Tab";
"PeerInfo.Tabs.SetMainTab.Succeed" = "Tab order changed.";
"MediaPlayer.SavedMusic.AddToProfile" = "Add to Profile";
"MediaPlayer.SavedMusic.RemoveFromProfile" = "This audio is visible on your profile. [Remove >]()";
"MediaPlayer.SavedMusic.AddedToProfile.View" = "View";
"MediaPlayer.SavedMusic.AddedToProfile" = "Audio added to your profile.";
"MediaPlayer.SavedMusic.RemovedFromProfile" = "Audio removed from your profile.";
"MediaPlayer.ContextMenu.SaveToFiles" = "Save to Files";
"MediaPlayer.ContextMenu.SaveTo" = "Save to...";
"MediaPlayer.ContextMenu.SaveTo.Profile" = "Profile";
"MediaPlayer.ContextMenu.SaveTo.SavedMessages" = "Saved Messages";
"MediaPlayer.ContextMenu.SaveTo.Files" = "Files";
"MediaPlayer.ContextMenu.SaveTo.Info" = "Choose where you want this audio to be saved.";
"MediaPlayer.ContextMenu.ShowInChat" = "Show in Chat";
"MediaPlayer.ContextMenu.Forward" = "Forward";
"MediaPlayer.ContextMenu.Delete" = "Delete";
"MediaPlayer.ContextMenu.Remove" = "Remove";
"MediaPlayer.Playlist.ThisChat" = "AUDIO IN THIS CHAT";
"MediaPlayer.Playlist.SavedMusic" = "%@'S PLAYLIST";
"MediaPlayer.Playlist.SavedMusicYou" = "YOUR PLAYLIST";
"Notification.PremiumGift.Stars_1" = "%@ Star";
"Notification.PremiumGift.Stars_any" = "%@ Stars";
"Ton.ProceedsOverview" = "PROCEEDS OVERVIEW";
"Ton.AvailableBalance" = "Balance Available to Withdraw";
"Ton.LifetimeProceeds" = "Total Lifetime Proceeds";
"Ton.WithdrawViaFragment" = "Withdraw via Fragment";
"Ton.WithdrawViaFragment.Info" = "Collect your TON using Fragment. [Learn More >]()";
"Ton.WithdrawViaFragment.Info_URL" = "https://telegram.org/tos/bot-developers#6-2-2-tpa-balance";
"MESSAGE_GIFT_THEME" = "%1$@ changed theme to %2$@";
"Gift.Upgrade.Skip" = "Skip";
"Gift.Upgrade.UpgradeNext" = "Upgrade Next Gift";
"Gift.Value.DescriptionAveragePrice" = "This is the average sale price of **%@** on Telegram and Fragment over the past month.";
"Gift.Value.DescriptionLastPriceFragment" = "This is the last price at which **%@** was last sold on Fragment.";
"Gift.Value.DescriptionLastPriceTelegram" = "This is the last price at which **%@** was last sold on Telegram.";
"Gift.Value.LastPriceInfo" = "**%1$@** is the last price for %2$@ gifts listed on Telegram and Fragment.";
"Gift.Value.MinimumPriceInfo" = "**%1$@** is the floor price for %2$@ gifts listed on Telegram and Fragment.";
"Gift.Value.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts listed on Telegram and Fragment over the past month.";
"Gift.Value.AveragePrice" = "Last Sale";
"Gift.Value.InitialSale" = "Initial Sale";
"Gift.Value.InitialPrice" = "Initial Price";
"Gift.Value.LastSale" = "Last Sale";
"Gift.Value.LastPrice" = "Last Price";
"Gift.Value.MinimumPrice" = "Minimum Price";
"Gift.Value.AveragePrice" = "Average Price";
"Gift.Value.ForSaleOnTelegram" = "for sale on Telegram";
"Gift.Value.ForSaleOnFragment" = "for sale on Fragment";
"Gift.View.Context.SetAsTheme" = "Set as Theme in...";
"Notification.GiftStars" = "Gift for %@";
"Notification.GiftStars.Stars_1" = "%@ Star";
"Notification.GiftStars.Stars_any" = "%@ Stars";
"Notification.PrepaidGiftUpgrade" = "Gift Upgrade for %@";
"Notification.PrepaidGiftUpgrade.Stars_1" = "%@ Star";
"Notification.PrepaidGiftUpgrade.Stars_any" = "%@ Stars";
"ProfileLevelInfo.RatingTitle" = "Rating";
"ProfileLevelInfo.FutureRatingTitle" = "Future Rating";

View File

@ -1326,7 +1326,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeStarsIntroScreen(context: AccountContext) -> ViewController
func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController
func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController
func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController

View File

@ -2503,7 +2503,7 @@ public final class ChatListNode: ListView {
case let .user(userType): case let .user(userType):
if case let .user(user) = peer { if case let .user(user) = peer {
match = true match = true
if user.id.isVerificationCodes { if user.id.isVerificationCodes || user.id.isTelegramNotifications {
match = false match = false
} }
if let isBot = userType.isBot { if let isBot = userType.isBot {

View File

@ -65,6 +65,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
public let clipsContent: Bool public let clipsContent: Bool
public let isScrollEnabled: Bool public let isScrollEnabled: Bool
public let hasDimView: Bool public let hasDimView: Bool
public let autoAnimateOut: Bool
public let externalState: ExternalState? public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>> public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void public let onPan: () -> Void
@ -77,6 +78,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
clipsContent: Bool = false, clipsContent: Bool = false,
isScrollEnabled: Bool = true, isScrollEnabled: Bool = true,
hasDimView: Bool = true, hasDimView: Bool = true,
autoAnimateOut: Bool = true,
externalState: ExternalState? = nil, externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>, animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {}, onPan: @escaping () -> Void = {},
@ -88,6 +90,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.clipsContent = clipsContent self.clipsContent = clipsContent
self.isScrollEnabled = isScrollEnabled self.isScrollEnabled = isScrollEnabled
self.hasDimView = hasDimView self.hasDimView = hasDimView
self.autoAnimateOut = autoAnimateOut
self.externalState = externalState self.externalState = externalState
self.animateOut = animateOut self.animateOut = animateOut
self.onPan = onPan self.onPan = onPan
@ -110,6 +113,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if lhs.hasDimView != rhs.hasDimView { if lhs.hasDimView != rhs.hasDimView {
return false return false
} }
if lhs.autoAnimateOut != rhs.autoAnimateOut {
return false
}
if lhs.animateOut != rhs.animateOut { if lhs.animateOut != rhs.animateOut {
return false return false
} }
@ -430,9 +436,15 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn() self.animateIn()
} else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) { } else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
self.animateOut(completion: {}) if component.autoAnimateOut {
self.animateOut(completion: {})
}
}
if !sheetEnvironment.isDisplaying && !component.autoAnimateOut {
} else {
self.previousIsDisplaying = sheetEnvironment.isDisplaying
} }
self.previousIsDisplaying = sheetEnvironment.isDisplaying
self.dismiss = sheetEnvironment.dismiss self.dismiss = sheetEnvironment.dismiss
return availableSize return availableSize

View File

@ -349,6 +349,8 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
public final var beganInteractiveDragging: (CGPoint) -> Void = { _ in } public final var beganInteractiveDragging: (CGPoint) -> Void = { _ in }
public final var endedInteractiveDragging: (CGPoint) -> Void = { _ in } public final var endedInteractiveDragging: (CGPoint) -> Void = { _ in }
public final var didEndScrolling: ((Bool) -> Void)? public final var didEndScrolling: ((Bool) -> Void)?
public final var didEndScrollingWithOverscroll: (() -> Void)?
private var currentGeneralScrollDirection: GeneralScrollDirection? private var currentGeneralScrollDirection: GeneralScrollDirection?
public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in } public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in }
@ -891,6 +893,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
self.resetScrollIndicatorFlashTimer(start: false) self.resetScrollIndicatorFlashTimer(start: false)
self.isAuxiliaryDisplayLinkEnabled = true self.isAuxiliaryDisplayLinkEnabled = true
if scrollView.contentOffset.y < -48.0 {
self.didEndScrollingWithOverscroll?()
}
} else { } else {
self.isDeceleratingAfterTracking = false self.isDeceleratingAfterTracking = false
self.resetHeaderItemsFlashTimer(start: true) self.resetHeaderItemsFlashTimer(start: true)

View File

@ -4,8 +4,28 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
@interface GiftPatternRect : NSObject
@property (nonatomic) CGPoint center;
@property (nonatomic) CGFloat side;
@property (nonatomic) CGFloat rotation;
@property (nonatomic) CGFloat scale;
@end
@interface GiftPatternData : NSObject
@property (nonatomic) CGSize size;
@property (nonatomic, strong) NSArray<GiftPatternRect *> * _Nonnull rects;
@end
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern); NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern);
GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data);
UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit); UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit);
UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex);
UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque); UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque);

File diff suppressed because it is too large Load Diff

View File

@ -959,7 +959,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) }
dict[-2136190013] = { return Api.StarGift.parse_starGift($0) } dict[-2136190013] = { return Api.StarGift.parse_starGift($0) }
dict[648369470] = { return Api.StarGift.parse_starGiftUnique($0) } dict[468707429] = { return Api.StarGift.parse_starGiftUnique($0) }
dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) }
dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) }
dict[-524291476] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) } dict[-524291476] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) }
@ -1230,7 +1230,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) } dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) }
dict[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($0) } dict[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($0) }
dict[-331111727] = { return Api.account.BusinessChatLinks.parse_businessChatLinks($0) } dict[-331111727] = { return Api.account.BusinessChatLinks.parse_businessChatLinks($0) }
dict[1271855483] = { return Api.account.ChatThemes.parse_chatThemes($0) } dict[373835863] = { return Api.account.ChatThemes.parse_chatThemes($0) }
dict[-535699004] = { return Api.account.ChatThemes.parse_chatThemesNotModified($0) } dict[-535699004] = { return Api.account.ChatThemes.parse_chatThemesNotModified($0) }
dict[400029819] = { return Api.account.ConnectedBots.parse_connectedBots($0) } dict[400029819] = { return Api.account.ConnectedBots.parse_connectedBots($0) }
dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) } dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) }

View File

@ -289,7 +289,7 @@ public extension Api {
public extension Api { public extension Api {
enum StarGift: TypeConstructorDescription { enum StarGift: TypeConstructorDescription {
case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?) case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?)
case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?) case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -315,9 +315,9 @@ public extension Api {
if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)}
break break
case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer):
if boxed { if boxed {
buffer.appendInt32(648369470) buffer.appendInt32(468707429)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false)
@ -344,6 +344,7 @@ public extension Api {
if Int(flags) & Int(1 << 5) != 0 {releasedBy!.serialize(buffer, true)} if Int(flags) & Int(1 << 5) != 0 {releasedBy!.serialize(buffer, true)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeString(valueCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(valueCurrency!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 10) != 0 {themePeer!.serialize(buffer, true)}
break break
} }
} }
@ -352,8 +353,8 @@ public extension Api {
switch self { switch self {
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate): case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate):
return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any)]) return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any)])
case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer):
return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any)]) return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any)])
} }
} }
@ -463,6 +464,10 @@ public extension Api {
if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() } if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() }
var _17: String? var _17: String?
if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) } if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) }
var _18: Api.Peer?
if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() {
_18 = Api.parse(reader, signature: signature) as? Api.Peer
} }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -480,8 +485,9 @@ public extension Api {
let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil
let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil
return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 {
return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18)
} }
else { else {
return nil return nil

View File

@ -570,14 +570,14 @@ public extension Api.account {
} }
public extension Api.account { public extension Api.account {
enum ChatThemes: TypeConstructorDescription { enum ChatThemes: TypeConstructorDescription {
case chatThemes(flags: Int32, hash: Int64, themes: [Api.ChatTheme], nextOffset: Int32?) case chatThemes(flags: Int32, hash: Int64, themes: [Api.ChatTheme], chats: [Api.Chat], users: [Api.User], nextOffset: Int32?)
case chatThemesNotModified case chatThemesNotModified
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .chatThemes(let flags, let hash, let themes, let nextOffset): case .chatThemes(let flags, let hash, let themes, let chats, let users, let nextOffset):
if boxed { if boxed {
buffer.appendInt32(1271855483) buffer.appendInt32(373835863)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(hash, buffer: buffer, boxed: false) serializeInt64(hash, buffer: buffer, boxed: false)
@ -586,6 +586,16 @@ public extension Api.account {
for item in themes { for item in themes {
item.serialize(buffer, true) item.serialize(buffer, true)
} }
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextOffset!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextOffset!, buffer: buffer, boxed: false)}
break break
case .chatThemesNotModified: case .chatThemesNotModified:
@ -599,8 +609,8 @@ public extension Api.account {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .chatThemes(let flags, let hash, let themes, let nextOffset): case .chatThemes(let flags, let hash, let themes, let chats, let users, let nextOffset):
return ("chatThemes", [("flags", flags as Any), ("hash", hash as Any), ("themes", themes as Any), ("nextOffset", nextOffset as Any)]) return ("chatThemes", [("flags", flags as Any), ("hash", hash as Any), ("themes", themes as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)])
case .chatThemesNotModified: case .chatThemesNotModified:
return ("chatThemesNotModified", []) return ("chatThemesNotModified", [])
} }
@ -615,14 +625,24 @@ public extension Api.account {
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChatTheme.self) _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChatTheme.self)
} }
var _4: Int32? var _4: [Api.Chat]?
if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _5: [Api.User]?
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
var _6: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 { let _c5 = _5 != nil
return Api.account.ChatThemes.chatThemes(flags: _1!, hash: _2!, themes: _3!, nextOffset: _4) let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.account.ChatThemes.chatThemes(flags: _1!, hash: _2!, themes: _3!, chats: _4!, users: _5!, nextOffset: _6)
} }
else { else {
return nil return nil

View File

@ -359,7 +359,9 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
resellForTonOnly: false, resellForTonOnly: false,
releasedBy: nil, releasedBy: nil,
valueAmount: nil, valueAmount: nil,
valueCurrency: nil valueCurrency: nil,
flags: [],
themePeerId: nil
) )
if let entry = CodableEntry(RecentStarGiftItem(gift)) { if let entry = CodableEntry(RecentStarGiftItem(gift)) {
items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry)) items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry))

View File

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

View File

@ -230,6 +230,37 @@ public enum TelegramWallpaper: Equatable {
self.file = file self.file = file
self.settings = settings self.settings = settings
} }
public static func ==(lhs: File, rhs: File) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.accessHash != rhs.accessHash {
return false
}
if lhs.isCreator != rhs.isCreator {
return false
}
if lhs.isDefault != rhs.isDefault {
return false
}
if lhs.isPattern != rhs.isPattern {
return false
}
if lhs.isDark != rhs.isDark {
return false
}
if lhs.slug != rhs.slug {
return false
}
if lhs.file.fileId != rhs.file.fileId {
return false
}
if lhs.settings != rhs.settings {
return false
}
return true
}
} }
case builtin(WallpaperSettings) case builtin(WallpaperSettings)

View File

@ -2303,6 +2303,39 @@ public extension TelegramEngine.EngineData.Item {
} }
} }
public struct CopyProtectionEnabled: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Bool
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .basicPeer(self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? BasicPeerView else {
preconditionFailure()
}
guard let peer = view.peer else {
return false
}
if let group = peer as? TelegramGroup {
return group.flags.contains(.copyProtectionEnabled)
} else if let channel = peer as? TelegramChannel {
return channel.flags.contains(.copyProtectionEnabled)
} else {
return false
}
}
}
public struct BotPreview: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public struct BotPreview: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = CachedUserData.BotPreview? public typealias Result = CachedUserData.BotPreview?

View File

@ -318,6 +318,18 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case releasedBy case releasedBy
case valueAmount case valueAmount
case valueCurrency case valueCurrency
case flags
case themePeerId
}
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let isThemeAvailable = Flags(rawValue: 1 << 0)
} }
public enum Attribute: Equatable, Codable, PostboxCoding { public enum Attribute: Equatable, Codable, PostboxCoding {
@ -593,8 +605,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let releasedBy: EnginePeer.Id? public let releasedBy: EnginePeer.Id?
public let valueAmount: Int64? public let valueAmount: Int64?
public let valueCurrency: String? public let valueCurrency: String?
public let flags: Flags
public let themePeerId: EnginePeer.Id?
public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?) { public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?) {
self.id = id self.id = id
self.giftId = giftId self.giftId = giftId
self.title = title self.title = title
@ -609,6 +623,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = releasedBy self.releasedBy = releasedBy
self.valueAmount = valueAmount self.valueAmount = valueAmount
self.valueCurrency = valueCurrency self.valueCurrency = valueCurrency
self.flags = flags
self.themePeerId = themePeerId
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -641,6 +657,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy) self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy)
self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount) self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount)
self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency) self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency)
self.flags = try container.decodeIfPresent(Int32.self, forKey: .flags).flatMap { Flags(rawValue: $0) } ?? []
self.themePeerId = try container.decodeIfPresent(Int64.self, forKey: .themePeerId).flatMap { EnginePeer.Id($0) }
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
@ -672,6 +690,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) } self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) }
self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue) self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue)
self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue) self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue)
self.flags = decoder.decodeOptionalInt32ForKey(CodingKeys.flags.rawValue).flatMap { Flags(rawValue: $0) } ?? []
self.themePeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.themePeerId.rawValue).flatMap { EnginePeer.Id($0) }
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -697,6 +717,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy) try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy)
try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount) try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount)
try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency) try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency)
try container.encode(self.flags.rawValue, forKey: .flags)
try container.encodeIfPresent(self.themePeerId?.toInt64(), forKey: .themePeerId)
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
@ -738,6 +760,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue) encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue)
encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue) encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue)
} }
encoder.encodeInt32(self.flags.rawValue, forKey: CodingKeys.flags.rawValue)
if let themePeerId = self.themePeerId {
encoder.encodeInt64(themePeerId.toInt64(), forKey: CodingKeys.themePeerId.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.themePeerId.rawValue)
}
} }
public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift { public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift {
@ -755,7 +783,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
resellForTonOnly: self.resellForTonOnly, resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy, releasedBy: self.releasedBy,
valueAmount: self.valueAmount, valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency valueCurrency: self.valueCurrency,
flags: self.flags,
themePeerId: self.themePeerId
) )
} }
@ -774,7 +804,30 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
resellForTonOnly: resellForTonOnly, resellForTonOnly: resellForTonOnly,
releasedBy: self.releasedBy, releasedBy: self.releasedBy,
valueAmount: self.valueAmount, valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency valueCurrency: self.valueCurrency,
flags: self.flags,
themePeerId: self.themePeerId
)
}
public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> UniqueGift {
return UniqueGift(
id: self.id,
giftId: self.giftId,
title: self.title,
number: self.number,
slug: self.slug,
owner: self.owner,
attributes: self.attributes,
availability: self.availability,
giftAddress: self.giftAddress,
resellAmounts: self.resellAmounts,
resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
flags: self.flags,
themePeerId: themePeerId
) )
} }
} }
@ -884,7 +937,7 @@ extension StarGift {
return nil return nil
} }
self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate)) self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate))
case let .starGiftUnique(flags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency): case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer):
let owner: StarGift.UniqueGift.Owner let owner: StarGift.UniqueGift.Owner
if let ownerAddress { if let ownerAddress {
owner = .address(ownerAddress) owner = .address(ownerAddress)
@ -896,7 +949,11 @@ extension StarGift {
return nil return nil
} }
let resellAmounts = resellAmounts?.compactMap { CurrencyAmount(apiAmount: $0) } let resellAmounts = resellAmounts?.compactMap { CurrencyAmount(apiAmount: $0) }
self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (flags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency)) var flags = StarGift.UniqueGift.Flags()
if (apiFlags & (1 << 9)) != 0 {
flags.insert(.isThemeAvailable)
}
self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId))
} }
} }
} }
@ -1322,6 +1379,7 @@ private final class ProfileGiftsContextImpl {
private var sorting: ProfileGiftsContext.Sorting private var sorting: ProfileGiftsContext.Sorting
private var filter: ProfileGiftsContext.Filters private var filter: ProfileGiftsContext.Filters
private var limit: Int32
private var gifts: [ProfileGiftsContext.State.StarGift] = [] private var gifts: [ProfileGiftsContext.State.StarGift] = []
private var count: Int32? private var count: Int32?
@ -1345,7 +1403,8 @@ private final class ProfileGiftsContextImpl {
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
collectionId: Int32?, collectionId: Int32?,
sorting: ProfileGiftsContext.Sorting, sorting: ProfileGiftsContext.Sorting,
filter: ProfileGiftsContext.Filters filter: ProfileGiftsContext.Filters,
limit: Int32
) { ) {
self.queue = queue self.queue = queue
self.account = account self.account = account
@ -1353,6 +1412,7 @@ private final class ProfileGiftsContextImpl {
self.collectionId = collectionId self.collectionId = collectionId
self.sorting = sorting self.sorting = sorting
self.filter = filter self.filter = filter
self.limit = limit
self.loadMore() self.loadMore()
} }
@ -1377,6 +1437,7 @@ private final class ProfileGiftsContextImpl {
let postbox = self.account.postbox let postbox = self.account.postbox
let filter = self.filter let filter = self.filter
let sorting = self.sorting let sorting = self.sorting
let limit = self.limit
let isFiltered = self.filter != .All || self.sorting != .date let isFiltered = self.filter != .All || self.sorting != .date
if !isFiltered { if !isFiltered {
@ -1464,7 +1525,7 @@ private final class ProfileGiftsContextImpl {
if !filter.contains(.unique) { if !filter.contains(.unique) {
flags |= (1 << 4) flags |= (1 << 4)
} }
return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: collectionId, offset: initialNextOffset ?? "", limit: 36)) return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: collectionId, offset: initialNextOffset ?? "", limit: limit))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.SavedStarGifts?, NoError> in |> `catch` { _ -> Signal<Api.payments.SavedStarGifts?, NoError> in
return .single(nil) return .single(nil)
@ -2363,14 +2424,15 @@ public final class ProfileGiftsContext {
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
collectionId: Int32? = nil, collectionId: Int32? = nil,
sorting: ProfileGiftsContext.Sorting = .date, sorting: ProfileGiftsContext.Sorting = .date,
filter: ProfileGiftsContext.Filters = .All filter: ProfileGiftsContext.Filters = .All,
limit: Int32 = 36
) { ) {
self.peerId = peerId self.peerId = peerId
self.collectionId = collectionId self.collectionId = collectionId
let queue = self.queue let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: { self.impl = QueueLocalObject(queue: queue, generate: {
return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, collectionId: collectionId, sorting: sorting, filter: filter) return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, collectionId: collectionId, sorting: sorting, filter: filter, limit: limit)
}) })
} }

View File

@ -1678,6 +1678,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
return .fail(.alreadyPaid) return .fail(.alreadyPaid)
} else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" {
return .fail(.starGiftOutOfStock) return .fail(.starGiftOutOfStock)
} else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" {
return .fail(.starGiftUserLimit)
} }
return .fail(.generic) return .fail(.generic)
} }

View File

@ -104,20 +104,20 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable {
} else { } else {
return false return false
} }
case let .gift(lhsGift, _): case let .gift(lhsGift, lhsThemeSettings):
if case let .gift(rhsGift, _) = rhs { if case let .gift(rhsGift, rhsThemeSettings) = rhs {
switch lhsGift { switch lhsGift {
case .generic(let lhsGeneric): case let .generic(lhsGeneric):
switch rhsGift { switch rhsGift {
case .generic(let rhsGeneric): case let .generic(rhsGeneric):
return lhsGeneric == rhsGeneric return lhsGeneric == rhsGeneric && lhsThemeSettings == rhsThemeSettings
default: default:
return false return false
} }
case .unique(let lhsUnique): case let .unique(lhsUnique):
switch rhsGift { switch rhsGift {
case .unique(let rhsUnique): case let .unique(rhsUnique):
return lhsUnique.id == rhsUnique.id return lhsUnique.slug == rhsUnique.slug && lhsThemeSettings == rhsThemeSettings
default: default:
return false return false
} }
@ -148,6 +148,20 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable {
} }
} }
} }
public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> ChatTheme {
switch self {
case .emoticon:
return self
case let .gift(gift, themeSettings):
switch gift {
case let .unique(uniqueGift):
return .gift(.unique(uniqueGift.withThemePeerId(themePeerId)), themeSettings)
case .generic:
return self
}
}
}
} }
extension ChatTheme { extension ChatTheme {
@ -235,6 +249,22 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe
return .complete() return .complete()
} }
return account.postbox.transaction { transaction -> Signal<Void, NoError> in return account.postbox.transaction { transaction -> Signal<Void, NoError> in
var chatTheme = chatTheme
if case let .gift(gift, _) = chatTheme, case let .unique(uniqueGift) = gift, let previousThemePeerId = uniqueGift.themePeerId {
transaction.updatePeerCachedData(peerIds: Set([previousThemePeerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedChatTheme(nil)
} else if let current = current as? CachedGroupData {
return current.withUpdatedChatTheme(nil)
} else if let current = current as? CachedChannelData {
return current.withUpdatedChatTheme(nil)
} else {
return current
}
})
}
chatTheme = chatTheme?.withThemePeerId(peerId)
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedUserData { if let current = current as? CachedUserData {
return current.withUpdatedChatTheme(chatTheme) return current.withUpdatedChatTheme(chatTheme)
@ -246,6 +276,7 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe
return current return current
} }
}) })
let inputTheme: Api.InputChatTheme let inputTheme: Api.InputChatTheme
if let chatTheme { if let chatTheme {
inputTheme = chatTheme.apiChatTheme inputTheme = chatTheme.apiChatTheme
@ -466,7 +497,7 @@ public final class UniqueGiftChatThemesContext {
private let cacheDisposable = MetaDisposable() private let cacheDisposable = MetaDisposable()
private var themes: [ChatTheme] = [] private var themes: [ChatTheme] = []
private var nextOffset: Int32? private var nextOffset: Int32 = 0
private var dataState: UniqueGiftChatThemesContext.State.DataState = .ready(canLoadMore: true) private var dataState: UniqueGiftChatThemesContext.State.DataState = .ready(canLoadMore: true)
private let stateValue = Promise<State>() private let stateValue = Promise<State>()
@ -487,7 +518,7 @@ public final class UniqueGiftChatThemesContext {
public func reload() { public func reload() {
self.themes = [] self.themes = []
self.nextOffset = nil self.nextOffset = 0
self.dataState = .ready(canLoadMore: true) self.dataState = .ready(canLoadMore: true)
self.loadMore(reload: true) self.loadMore(reload: true)
} }
@ -495,6 +526,7 @@ public final class UniqueGiftChatThemesContext {
public func loadMore(reload: Bool = false) { public func loadMore(reload: Bool = false) {
let network = self.account.network let network = self.account.network
let postbox = self.account.postbox let postbox = self.account.postbox
let accountPeerId = self.account.peerId
let dataState = self.dataState let dataState = self.dataState
let offset = self.nextOffset let offset = self.nextOffset
@ -520,13 +552,24 @@ public final class UniqueGiftChatThemesContext {
self.pushState() self.pushState()
} }
let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset ?? 0, limit: 32, hash: 0)) let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 50, hash: 0))
|> map { result -> ([ChatTheme], Int32?) in |> map(Optional.init)
switch result { |> `catch` { error in
case let .chatThemes(_, _, themes, nextOffset): return .single(nil)
return (themes.compactMap { ChatTheme(apiChatTheme: $0) }, nextOffset) }
case .chatThemesNotModified: |> mapToSignal { result -> Signal<([ChatTheme], Int32?), NoError> in
return ([], nil) guard let result else {
return .single(([], nil))
}
return postbox.transaction { transaction -> ([ChatTheme], Int32?) in
switch result {
case let .chatThemes(_, _, themes, chats, users, nextOffset):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
return (themes.compactMap { ChatTheme(apiChatTheme: $0) }, nextOffset)
case .chatThemesNotModified:
return ([], nil)
}
} }
} }
@ -545,7 +588,9 @@ public final class UniqueGiftChatThemesContext {
} else { } else {
self.themes.append(contentsOf: themes) self.themes.append(contentsOf: themes)
} }
if let nextOffset {
self.nextOffset = nextOffset
}
self.dataState = .ready(canLoadMore: nextOffset != nil) self.dataState = .ready(canLoadMore: nextOffset != nil)
self.pushState() self.pushState()
})) }))

View File

@ -47,13 +47,36 @@ public func makePresentationTheme(cloudTheme: TelegramTheme, dark: Bool = false)
} else { } else {
settings = nil settings = nil
} }
guard let settings = settings else { guard let settings else {
return nil return nil
} }
let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), extendingThemeReference: .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: nil)), serviceBackgroundColor: nil, preview: false) let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), extendingThemeReference: .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: nil)), serviceBackgroundColor: nil, preview: false)
return customizePresentationTheme(defaultTheme, editing: true, accentColor: UIColor(argb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(argb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper) return customizePresentationTheme(defaultTheme, editing: true, accentColor: UIColor(argb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(argb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper)
} }
public func makePresentationTheme(chatTheme: ChatTheme, dark: Bool = false) -> PresentationTheme? {
guard case let .gift(_, themeSettings) = chatTheme else {
return nil
}
let settings: TelegramThemeSettings?
if let exactSettings = themeSettings.first(where: { dark ? ($0.baseTheme == .night || $0.baseTheme == .tinted) : ($0.baseTheme == .classic || $0.baseTheme == .day) }) {
settings = exactSettings
} else if let firstSettings = themeSettings.first {
settings = firstSettings
} else {
settings = nil
}
guard let settings else {
return nil
}
let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), serviceBackgroundColor: nil, preview: false)
let theme = customizePresentationTheme(defaultTheme, editing: false, accentColor: UIColor(rgb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(rgb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper)
if case let .gift(starGiftValue, _) = chatTheme {
theme.starGift = starGiftValue
}
return theme
}
public func makePresentationTheme(cloudTheme: TelegramTheme, baseTheme: TelegramBaseTheme? = nil) -> PresentationTheme? { public func makePresentationTheme(cloudTheme: TelegramTheme, baseTheme: TelegramBaseTheme? = nil) -> PresentationTheme? {
let settings: TelegramThemeSettings? let settings: TelegramThemeSettings?
if let exactSettings = cloudTheme.settings?.first(where: { $0.baseTheme == baseTheme }) { if let exactSettings = cloudTheme.settings?.first(where: { $0.baseTheme == baseTheme }) {

View File

@ -1574,6 +1574,7 @@ public final class PresentationTheme: Equatable {
public let chart: PresentationThemeChart public let chart: PresentationThemeChart
public let preview: Bool public let preview: Bool
public var forceSync: Bool = false public var forceSync: Bool = false
public var starGift: StarGift?
public let resourceCache: PresentationsResourceCache = PresentationsResourceCache() public let resourceCache: PresentationsResourceCache = PresentationsResourceCache()

View File

@ -781,12 +781,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else { } else {
var emoji = "" var emoji = ""
var additionalAttributes: [String: Any] = [:] var additionalAttributes: [String: Any] = [:]
var giftTitle: String?
switch chatTheme { switch chatTheme {
case let .emoticon(emoticon): case let .emoticon(emoticon):
emoji = emoticon emoji = emoticon
case let .gift(starGift, _): case let .gift(starGift, _):
var file: TelegramMediaFile? var file: TelegramMediaFile?
if case let .unique(uniqueGift) = starGift { if case let .unique(uniqueGift) = starGift {
giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: dateTimeFormat))"
for attribute in uniqueGift.attributes { for attribute in uniqueGift.attributes {
if case let .model(_, fileValue, _) = attribute { if case let .model(_, fileValue, _) = attribute {
file = fileValue file = fileValue
@ -802,11 +805,21 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
if message.author?.id.namespace == Namespaces.Peer.CloudChannel { if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
attributedString = NSAttributedString(string: strings.Notification_ChannelChangedTheme(emoji).string, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.Notification_ChannelChangedTheme(emoji).string, font: titleFont, textColor: primaryTextColor)
} else if message.author?.id == accountPeerId { } else if message.author?.id == accountPeerId {
let resultTitleString = strings.Notification_YouChangedTheme(emoji) if let giftTitle {
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: emojiAttributes]) let resultTitleString = strings.Notification_YouChangedThemeGift(giftTitle)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [:])
} else {
let resultTitleString = strings.Notification_YouChangedTheme(emoji)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: emojiAttributes])
}
} else { } else {
let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji) if let giftTitle {
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: emojiAttributes]) let resultTitleString = strings.Notification_ChangedThemeGift(compactAuthorName, giftTitle)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
} else {
let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: emojiAttributes])
}
} }
} }
case let .webViewData(text): case let .webViewData(text):
@ -817,8 +830,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else { } else {
let price: String let price: String
if currency == "XTR" { if currency == "XTR" {
//TODO:localize price = strings.Notification_PremiumGift_Stars(Int32(clamping: amount))
price = "\(amount) Stars"
} else { } else {
price = formatCurrencyAmount(amount, currency: currency) price = formatCurrencyAmount(amount, currency: currency)
} }
@ -833,7 +845,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case let .giftStars(currency, amount, count, _, _, _): case let .giftStars(currency, amount, count, _, _, _):
let _ = count let _ = count
if !forAdditionalServiceMessage { if !forAdditionalServiceMessage {
attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) let starsPrice = strings.Notification_GiftStars_Stars(Int32(clamping: count))
attributedString = NSAttributedString(string: strings.Notification_GiftStars(starsPrice).string, font: titleFont, textColor: primaryTextColor)
} else { } else {
let price = formatCurrencyAmount(amount, currency: currency) let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId { if message.author?.id == accountPeerId {
@ -1155,17 +1168,22 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = mutableString attributedString = mutableString
case .prizeStars: case .prizeStars:
attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor)
case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, _): case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate):
if !forAdditionalServiceMessage { if !forAdditionalServiceMessage {
if let text { if let text {
let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()))
attributedString = mutableAttributedString attributedString = mutableAttributedString
} else { } else {
attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) if isPrepaidUpgrade {
let starsPrice = strings.Notification_PrepaidGiftUpgrade_Stars(Int32(clamping: upgradeStars ?? 0))
attributedString = NSAttributedString(string: strings.Notification_PrepaidGiftUpgrade(starsPrice).string, font: titleFont, textColor: primaryTextColor)
} else {
attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor)
}
} }
} else if case let .generic(gift) = gift { } else if case let .generic(gift) = gift {
var finalPrice = gift.price var finalPrice = gift.price
if let upgradeStars { if let upgradeStars, !upgradeSeparate {
finalPrice += upgradeStars finalPrice += upgradeStars
} }
let starsPrice = strings.Notification_StarsGift_Stars(Int32(clamping: finalPrice)) let starsPrice = strings.Notification_StarsGift_Stars(Int32(clamping: finalPrice))

View File

@ -485,6 +485,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/FaceScanScreen", "//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ContactsHelper", "//submodules/ContactsHelper",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -241,6 +241,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} else if case .joinedChannel = action.action { } else if case .joinedChannel = action.action {
result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false needReactions = false
} else if case let .setChatTheme(chatTheme) = action.action, case .gift = chatTheme {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else { } else {
if !canAddMessageReactions(message: message) { if !canAddMessageReactions(message: message) {
needReactions = false needReactions = false

View File

@ -33,6 +33,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/InvisibleInkDustNode", "//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -24,6 +24,7 @@ import ChatMessageItemCommon
import TextNodeWithEntities import TextNodeWithEntities
import InvisibleInkDustNode import InvisibleInkDustNode
import PeerInfoCoverComponent import PeerInfoCoverComponent
import GiftItemComponent
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { 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) return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
@ -45,6 +46,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var dustNode: InvisibleInkDustNode? private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode private let animationNode: AnimatedStickerNode
private let giftIcon = ComponentView<Empty>()
private let modelTitleTextNode: TextNode private let modelTitleTextNode: TextNode
private let modelValueTextNode: TextNode private let modelValueTextNode: TextNode
@ -416,6 +418,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var months: Int32 = 3 var months: Int32 = 3
var animationName: String = "" var animationName: String = ""
var animationFile: TelegramMediaFile? var animationFile: TelegramMediaFile?
var uniqueGift: StarGift.UniqueGift?
var title = item.presentationData.strings.Notification_PremiumGift_Title var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = "" var text = ""
var subtitleColor = primaryTextColor var subtitleColor = primaryTextColor
@ -708,6 +711,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded
animationFile = gift.file animationFile = gift.file
} }
case let .setChatTheme(chatTheme):
title = ""
var giftTitle = ""
if case let .gift(gift, _) = chatTheme, case let .unique(uniqueGiftValue) = gift {
giftTitle = "\(uniqueGiftValue.title) #\(formatCollectibleNumber(uniqueGiftValue.number, dateTimeFormat: item.presentationData.dateTimeFormat))"
uniqueGift = uniqueGiftValue
}
if incoming {
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
text = item.presentationData.strings.Notification_ChatTheme_Text(authorName, giftTitle).string
} else {
text = item.presentationData.strings.Notification_ChatTheme_TextYou(giftTitle).string
}
hasServiceMessage = false
default: default:
break break
} }
@ -866,6 +883,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
giftSize.height += 12.0 giftSize.height += 12.0
} }
if let _ = uniqueGift {
giftSize.height -= 31.0
}
var labelRects = labelLayout.linesRects() var labelRects = labelLayout.linesRects()
if labelRects.count > 1 { if labelRects.count > 1 {
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
@ -944,7 +965,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.creatorButtonNode.isUserInteractionEnabled = !item.presentationData.isPreview strongSelf.creatorButtonNode.isUserInteractionEnabled = !item.presentationData.isPreview
strongSelf.creatorButtonTitleNode.isHidden = creatorButtonTitle.isEmpty strongSelf.creatorButtonTitleNode.isHidden = creatorButtonTitle.isEmpty
if strongSelf.item == nil && !isStoryEntity { if strongSelf.item == nil && !isStoryEntity && uniqueGift == nil {
strongSelf.animationNode.started = { [weak self] in strongSelf.animationNode.started = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let current = CACurrentMediaTime() let current = CACurrentMediaTime()
@ -1009,7 +1030,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame strongSelf.titleNode.frame = titleFrame
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) var clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight))
if let _ = uniqueGift {
clippingTextFrame.origin.y -= 23.0
}
var attributesOffsetY: CGFloat = 0.0 var attributesOffsetY: CGFloat = 0.0
@ -1315,6 +1339,31 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
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)
}
}
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
if let (offset, image) = backgroundMaskImage { if let (offset, image) = backgroundMaskImage {
if strongSelf.backgroundNode == nil { if strongSelf.backgroundNode == nil {
@ -1524,7 +1573,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if isPlaying { if isPlaying {
var alreadySeen = true var alreadySeen = true
if item.message.flags.contains(.Incoming) { if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case .setChatTheme = action.action {
} else if item.message.flags.contains(.Incoming) {
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
if unreadRange.contains(item.message.id.id) { if unreadRange.contains(item.message.id.id) {
alreadySeen = false alreadySeen = false

View File

@ -1656,7 +1656,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .screen) return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .screen)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value = value { if let value = value {
return .single(value) return .single(value.generator)
} else { } else {
return .complete() return .complete()
} }

View File

@ -387,7 +387,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
updateImageSignal = patternWallpaperImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, representations: representations, mode: .thumbnail) updateImageSignal = patternWallpaperImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, representations: representations, mode: .thumbnail)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value { if let value {
return .single(value) return .single(value.generator)
} else { } else {
return .complete() return .complete()
} }

View File

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatThemeScreen",
module_name = "ChatThemeScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/PresentationDataUtils",
"//submodules/TelegramNotices",
"//submodules/AnimationUI",
"//submodules/MergeLists",
"//submodules/MediaResources",
"//submodules/StickerResources",
"//submodules/WallpaperResources",
"//submodules/TooltipUI",
"//submodules/SolidRoundedButtonNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ShimmerEffect",
"//submodules/AttachmentUI",
"//submodules/AvatarNode",
"//submodules/Markdown",
"//submodules/AppBundle",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -21,20 +21,22 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ShimmerEffect import ShimmerEffect
import AttachmentUI import AttachmentUI
import AvatarNode
private struct ThemeSettingsThemeEntry: Comparable, Identifiable { private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
let index: Int let index: Int
let chatTheme: ChatTheme? let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile? let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference? let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool let nightMode: Bool
var selected: Bool var selected: Bool
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let wallpaper: TelegramWallpaper? let wallpaper: TelegramWallpaper?
var stableId: Int { var stableId: String {
return index return self.chatTheme?.id ?? "\(self.index)"
} }
static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool {
@ -47,6 +49,9 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
if lhs.themeReference?.index != rhs.themeReference?.index { if lhs.themeReference?.index != rhs.themeReference?.index {
return false return false
} }
if lhs.peer != rhs.peer {
return false
}
if lhs.nightMode != rhs.nightMode { if lhs.nightMode != rhs.nightMode {
return false return false
} }
@ -70,16 +75,16 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
} }
func item(context: AccountContext, action: @escaping (ChatTheme?) -> Void) -> ListViewItem { func item(context: AccountContext, action: @escaping (ChatTheme?) -> Void) -> ListViewItem {
return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, peer: self.peer, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action)
} }
} }
private class ThemeSettingsThemeIconItem: ListViewItem { private class ThemeSettingsThemeIconItem: ListViewItem {
let context: AccountContext let context: AccountContext
let chatTheme: ChatTheme? let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile? let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference? let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool let nightMode: Bool
let selected: Bool let selected: Bool
let theme: PresentationTheme let theme: PresentationTheme
@ -87,11 +92,24 @@ private class ThemeSettingsThemeIconItem: ListViewItem {
let wallpaper: TelegramWallpaper? let wallpaper: TelegramWallpaper?
let action: (ChatTheme?) -> Void let action: (ChatTheme?) -> Void
public init(context: AccountContext, chatTheme: ChatTheme?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (ChatTheme?) -> Void) { public init(
context: AccountContext,
chatTheme: ChatTheme?,
emojiFile: TelegramMediaFile?,
themeReference: PresentationThemeReference?,
peer: EnginePeer?,
nightMode: Bool,
selected: Bool,
theme: PresentationTheme,
strings: PresentationStrings,
wallpaper: TelegramWallpaper?,
action: @escaping (ChatTheme?) -> Void
) {
self.context = context self.context = context
self.chatTheme = chatTheme self.chatTheme = chatTheme
self.emojiFile = emojiFile self.emojiFile = emojiFile
self.themeReference = themeReference self.themeReference = themeReference
self.peer = peer
self.nightMode = nightMode self.nightMode = nightMode
self.selected = selected self.selected = selected
self.theme = theme self.theme = theme
@ -240,6 +258,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
private let emojiImageNode: TransformImageNode private let emojiImageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode? private var animatedStickerNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode private var placeholderNode: StickerShimmerEffectNode
private var bubbleNode: ASImageNode?
private var avatarNode: AvatarNode?
private var replaceNode: ASImageNode?
var snapshotView: UIView? var snapshotView: UIView?
var item: ThemeSettingsThemeIconItem? var item: ThemeSettingsThemeIconItem?
@ -489,6 +510,75 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
animatedStickerNode.frame = emojiFrame animatedStickerNode.frame = emojiFrame
animatedStickerNode.updateLayout(size: emojiFrame.size) animatedStickerNode.updateLayout(size: emojiFrame.size)
} }
if let _ = item.peer {
let bubbleNode: ASImageNode
if let current = strongSelf.bubbleNode {
bubbleNode = current
} else {
bubbleNode = ASImageNode()
strongSelf.insertSubnode(bubbleNode, belowSubnode: strongSelf.emojiContainerNode)
strongSelf.bubbleNode = bubbleNode
var bubbleColor: UIColor?
if let theme = item.chatTheme, case let .gift(_, themeSettings) = theme {
if item.nightMode {
if let theme = themeSettings.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted }) {
let color = theme.wallpaper?.settings?.colors.first ?? theme.accentColor
bubbleColor = UIColor(rgb: UInt32(bitPattern: color))
}
} else {
if let theme = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day }) {
let color = theme.wallpaper?.settings?.colors.first ?? theme.accentColor
bubbleColor = UIColor(rgb: UInt32(bitPattern: color))
}
}
}
if let bubbleColor {
bubbleNode.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 48.0), cornerRadius: 12.0, color: bubbleColor)
}
}
bubbleNode.frame = CGRect(origin: CGPoint(x: 50.0, y: 12.0), size: CGSize(width: 24.0, height: 48.0))
} else if let bubbleNode = strongSelf.bubbleNode {
strongSelf.bubbleNode = nil
bubbleNode.removeFromSupernode()
}
if let peer = item.peer {
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
strongSelf.insertSubnode(avatarNode, belowSubnode: strongSelf.emojiContainerNode)
strongSelf.avatarNode = avatarNode
avatarNode.setPeer(context: item.context, theme: item.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
}
avatarNode.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
avatarNode.frame = CGRect(origin: CGPoint(x: 52.0, y: 14.0), size: CGSize(width: 20.0, height: 20.0))
} else if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
if let _ = item.peer {
let replaceNode: ASImageNode
if let current = strongSelf.replaceNode {
replaceNode = current
} else {
replaceNode = ASImageNode()
strongSelf.insertSubnode(replaceNode, belowSubnode: strongSelf.emojiContainerNode)
strongSelf.replaceNode = replaceNode
replaceNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/Refresh"), color: .white)
}
replaceNode.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
if let image = replaceNode.image {
replaceNode.frame = CGRect(origin: CGPoint(x: 53.0, y: 37.0), size: image.size)
}
} else if let replaceNode = strongSelf.replaceNode {
strongSelf.replaceNode = nil
replaceNode.removeFromSupernode()
}
} }
}) })
} }
@ -525,9 +615,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
} }
} }
final class ChatThemeScreen: ViewController { public final class ChatThemeScreen: ViewController {
static let themeCrossfadeDuration: Double = 0.3 public static let themeCrossfadeDuration: Double = 0.3
static let themeCrossfadeDelay: Double = 0.25 public static let themeCrossfadeDelay: Double = 0.25
private var controllerNode: ChatThemeScreenNode { private var controllerNode: ChatThemeScreenNode {
return self.displayNode as! ChatThemeScreenNode return self.displayNode as! ChatThemeScreenNode
@ -539,7 +629,7 @@ final class ChatThemeScreen: ViewController {
private let animatedEmojiStickers: [String: [StickerPackItem]] private let animatedEmojiStickers: [String: [StickerPackItem]]
private let initiallySelectedTheme: ChatTheme? private let initiallySelectedTheme: ChatTheme?
private let peerName: String private let peerName: String
let canResetWallpaper: Bool fileprivate let canResetWallpaper: Bool
private let previewTheme: (ChatTheme?, Bool?) -> Void private let previewTheme: (ChatTheme?, Bool?) -> Void
fileprivate let changeWallpaper: () -> Void fileprivate let changeWallpaper: () -> Void
fileprivate let resetWallpaper: () -> Void fileprivate let resetWallpaper: () -> Void
@ -548,9 +638,9 @@ final class ChatThemeScreen: ViewController {
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
var dismissed: (() -> Void)? public var dismissed: (() -> Void)?
var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { public var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
didSet { didSet {
if self.isNodeLoaded { if self.isNodeLoaded {
self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl
@ -558,7 +648,7 @@ final class ChatThemeScreen: ViewController {
} }
} }
init( public init(
context: AccountContext, context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>),
animatedEmojiStickers: [String: [StickerPackItem]], animatedEmojiStickers: [String: [StickerPackItem]],
@ -657,7 +747,7 @@ final class ChatThemeScreen: ViewController {
} }
} }
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.forEachController({ controller in self.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
controller.dismiss() controller.dismiss()
@ -683,7 +773,7 @@ final class ChatThemeScreen: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
} }
func dimTapped() { public func dimTapped() {
self.controllerNode.dimTapped() self.controllerNode.dimTapped()
} }
} }
@ -743,6 +833,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
private var initialized = false private var initialized = false
private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext
private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State?
private let peerName: String private let peerName: String
@ -875,9 +966,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
self.cancelButtonNode.buttonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.cancelButtonNode.buttonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.doneButton.pressed = { [weak self] in self.doneButton.pressed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.doneButton.isUserInteractionEnabled = false
if strongSelf.doneButton.font == .bold { if strongSelf.doneButton.font == .bold {
strongSelf.completion?(strongSelf.selectedTheme) strongSelf.complete()
} else { } else {
strongSelf.controller?.changeWallpaper() strongSelf.controller?.changeWallpaper()
} }
@ -888,13 +978,37 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
self.disposable.set(combineLatest( self.disposable.set(combineLatest(
queue: Queue.mainQueue(), queue: Queue.mainQueue(),
self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager),
self.uniqueGiftChatThemesContext.state, self.uniqueGiftChatThemesContext.state
|> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in
var peerIds: [EnginePeer.Id] = []
for theme in state.themes {
if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId {
peerIds.append(themePeerId)
}
}
return combineLatest(
.single(state),
context.engine.data.get(
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
) |> map { peers in
var result: [EnginePeer.Id: EnginePeer] = [:]
for peerId in peerIds {
if let maybePeer = peers[peerId], let peer = maybePeer {
result[peerId] = peer
}
}
return result
}
)
},
self.selectedThemePromise.get(), self.selectedThemePromise.get(),
self.isDarkAppearancePromise.get() self.isDarkAppearancePromise.get()
).startStrict(next: { [weak self] themes, uniqueGiftChatThemes, selectedTheme, isDarkAppearance in ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let (uniqueGiftChatThemesState, peers) = uniqueGiftChatThemesStateAndPeers
strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState
let isFirstTime = strongSelf.entries == nil let isFirstTime = strongSelf.entries == nil
let presentationData = strongSelf.presentationData let presentationData = strongSelf.presentationData
@ -905,51 +1019,81 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
chatTheme: nil, chatTheme: nil,
emojiFile: nil, emojiFile: nil,
themeReference: nil, themeReference: nil,
peer: nil,
nightMode: false, nightMode: false,
selected: selectedTheme == nil, selected: selectedTheme == nil,
theme: presentationData.theme, theme: presentationData.theme,
strings: presentationData.strings, strings: presentationData.strings,
wallpaper: nil wallpaper: nil
)) ))
for theme in themes {
guard let emoticon = theme.emoticon else { var giftThemes = uniqueGiftChatThemesState.themes
continue var existingIds = Set<String>()
if let initiallySelectedTheme, case .gift = initiallySelectedTheme {
let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id })
if initialThemeIndex == nil || initialThemeIndex! > 50 {
giftThemes.insert(initiallySelectedTheme, at: 0)
} }
entries.append(ThemeSettingsThemeEntry(
index: entries.count,
chatTheme: .emoticon(emoticon),
emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(),
themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)),
nightMode: isDarkAppearance,
selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
} }
for theme in uniqueGiftChatThemes.themes {
guard case let .gift(gift, wallpaperFile) = theme else { for theme in giftThemes {
guard case let .gift(gift, themeSettings) = theme, !existingIds.contains(theme.id) else {
continue continue
} }
var emojiFile: TelegramMediaFile? var emojiFile: TelegramMediaFile?
var peer: EnginePeer?
if case let .unique(uniqueGift) = gift { if case let .unique(uniqueGift) = gift {
for attribute in uniqueGift.attributes { for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute { if case let .model(_, file, _) = attribute {
emojiFile = file emojiFile = file
} }
} }
if let themePeerId = uniqueGift.themePeerId, theme.id != initiallySelectedTheme?.id {
peer = peers[themePeerId]
}
}
let themeReference: PresentationThemeReference
let wallpaper: TelegramWallpaper?
if isDarkAppearance {
wallpaper = themeSettings.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted })?.wallpaper
themeReference = .builtin(.night)
} else {
wallpaper = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day })?.wallpaper
themeReference = .builtin(.dayClassic)
} }
entries.append(ThemeSettingsThemeEntry( entries.append(ThemeSettingsThemeEntry(
index: entries.count, index: entries.count,
chatTheme: theme, chatTheme: theme,
emojiFile: emojiFile, emojiFile: emojiFile,
themeReference: nil, themeReference: themeReference,
peer: peer,
nightMode: isDarkAppearance, nightMode: isDarkAppearance,
selected: selectedTheme?.id == theme.id, selected: selectedTheme?.id == theme.id,
theme: presentationData.theme, theme: presentationData.theme,
strings: presentationData.strings, strings: presentationData.strings,
wallpaper: .file(TelegramWallpaper.File(id: wallpaperFile.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: "", file: wallpaperFile, settings: WallpaperSettings(blur: false, motion: false, colors: [], intensity: 100, rotation: 0))) wallpaper: wallpaper
)) ))
existingIds.insert(theme.id)
}
if uniqueGiftChatThemesState.themes.count == 0 || uniqueGiftChatThemesState.dataState == .ready(canLoadMore: false) {
for theme in themes {
guard let emoticon = theme.emoticon else {
continue
}
entries.append(ThemeSettingsThemeEntry(
index: entries.count,
chatTheme: .emoticon(emoticon),
emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(),
themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)),
peer: nil,
nightMode: isDarkAppearance,
selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
}
} }
let action: (ChatTheme?) -> Void = { [weak self] chatTheme in let action: (ChatTheme?) -> Void = { [weak self] chatTheme in
@ -958,8 +1102,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
} }
} }
let previousEntries = strongSelf.entries ?? [] let previousEntries = strongSelf.entries ?? []
let crossfade = previousEntries.count != entries.count //let crossfade = previousEntries.count != entries.count
let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: false)
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
strongSelf.entries = entries strongSelf.entries = entries
@ -1008,6 +1152,15 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
} }
} }
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else {
return
}
if case let .known(value) = offset, value < 100.0 {
self.uniqueGiftChatThemesContext.loadMore()
}
}
self.updateCancelButton() self.updateCancelButton()
} }
@ -1195,13 +1348,39 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
} }
} }
func complete() {
let proceed = {
self.doneButton.isUserInteractionEnabled = false
self.completion?(self.selectedTheme)
}
if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
let controller = giftThemeTransferAlertController(
context: self.context,
gift: uniqueGift,
previousPeer: peer,
commit: {
proceed()
}
)
self.controller?.present(controller, in: .window(.root))
})
} else {
proceed()
}
}
func dimTapped() { func dimTapped() {
if self.selectedTheme?.id == self.initiallySelectedTheme?.id { if self.selectedTheme?.id == self.initiallySelectedTheme?.id {
self.cancelButtonPressed() self.cancelButtonPressed()
} else { } else {
let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in
if let strongSelf = self { if let self {
strongSelf.completion?(strongSelf.selectedTheme) self.complete()
} }
})], actionLayout: .horizontal, dismissOnOutsideTap: true) })], actionLayout: .horizontal, dismissOnOutsideTap: true)
self.present?(alertController) self.present?(alertController)

View File

@ -0,0 +1,295 @@
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 ActivityIndicator
private final class GiftThemeTransferAlertContentNode: 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 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: "Media Editor/CutoutUndo"), 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)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
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) - 52.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.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) + 52.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 += 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
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom)
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
}
separatorIndex += 1
let currentActionWidth: CGFloat
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
let actionNodeFrame: CGRect
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
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.isHidden = true
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 giftThemeTransferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
previousPeer: EnginePeer,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var contentNode: GiftThemeTransferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: {
dismissImpl?(true)
commit()
})]
let text = strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string
contentNode = GiftThemeTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: previousPeer, title: "", text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View File

@ -477,7 +477,12 @@ final class GiftSetupScreenComponent: Component {
case .starGiftUserLimit: case .starGiftUserLimit:
if let perUserLimit, let giftFile { if let perUserLimit, let giftFile {
let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit) let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit)
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), action: { _ in return false }) let undoController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil),
elevatedLayout: true,
action: { _ in return false }
)
controller.present(undoController, in: .current) controller.present(undoController, in: .current)
return return
} }

View File

@ -277,7 +277,7 @@ final class GiftStoreScreenComponent: Component {
buyGift: { slug, peerId, price in buyGift: { slug, peerId, price in
return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete() return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete()
}, },
updateResellStars: { price in updateResellStars: { _, price in
return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete()
} }
) )

View File

@ -53,6 +53,7 @@ swift_library(
"//submodules/ActivityIndicator", "//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ImageBlur", "//submodules/ImageBlur",
], ],
visibility = [ visibility = [

View File

@ -149,10 +149,6 @@ private final class GiftValueSheetContent: CombinedComponent {
gift: gift gift: gift
) )
controller.push(storeController) controller.push(storeController)
Queue.mainQueue().after(2.0, {
controller.dismiss(animated: false)
})
} }
func openGiftFragmentResale(url: String) { func openGiftFragmentResale(url: String) {
@ -205,8 +201,6 @@ private final class GiftValueSheetContent: CombinedComponent {
let theme = environment.theme let theme = environment.theme
let strings = environment.strings let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat let dateTimeFormat = environment.dateTimeFormat
//let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
//let controller = environment.controller
let state = context.state let state = context.state
@ -319,12 +313,12 @@ private final class GiftValueSheetContent: CombinedComponent {
var descriptionText: String var descriptionText: String
if component.valueInfo.valueIsAverage { if component.valueInfo.valueIsAverage {
descriptionText = "This is the average sale price of **\(giftCollectionTitle)** on Telegram and Fragment over the past month." descriptionText = strings.Gift_Value_DescriptionAveragePrice(giftCollectionTitle).string
} else { } else {
if component.valueInfo.isLastSaleOnFragment { if component.valueInfo.isLastSaleOnFragment {
descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Fragment." descriptionText = strings.Gift_Value_DescriptionLastPriceFragment(giftTitle).string
} else { } else {
descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Telegram." descriptionText = strings.Gift_Value_DescriptionLastPriceTelegram(giftTitle).string
} }
} }
if !descriptionText.isEmpty { if !descriptionText.isEmpty {
@ -394,7 +388,7 @@ private final class GiftValueSheetContent: CombinedComponent {
tableItems.append(.init( tableItems.append(.init(
id: "initialDate", id: "initialDate",
title: "Initial Sale", title: strings.Gift_Value_InitialSale,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
) )
@ -410,7 +404,7 @@ private final class GiftValueSheetContent: CombinedComponent {
tableItems.append(.init( tableItems.append(.init(
id: "initialPrice", id: "initialPrice",
title: "Initial Price", title: strings.Gift_Value_InitialPrice,
component: AnyComponent(MultilineTextWithEntitiesComponent( component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context, context: component.context,
animationCache: component.context.animationCache, animationCache: component.context.animationCache,
@ -425,7 +419,7 @@ private final class GiftValueSheetContent: CombinedComponent {
if let lastSaleDate = component.valueInfo.lastSaleDate { if let lastSaleDate = component.valueInfo.lastSaleDate {
tableItems.append(.init( tableItems.append(.init(
id: "lastDate", id: "lastDate",
title: "Last Sale", title: strings.Gift_Value_LastSale,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
) )
@ -457,7 +451,7 @@ private final class GiftValueSheetContent: CombinedComponent {
color: theme.list.itemAccentColor color: theme.list.itemAccentColor
)), )),
action: { [weak state] in action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(lastSalePriceString)** is the last price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.") state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_LastPriceInfo(lastSalePriceString, giftCollectionTitle).string)
} }
).tagged(tag)) ).tagged(tag))
@ -467,7 +461,7 @@ private final class GiftValueSheetContent: CombinedComponent {
) )
tableItems.append(.init( tableItems.append(.init(
id: "lastPrice", id: "lastPrice",
title: "Last Price", title: strings.Gift_Value_LastPrice,
hasBackground: false, hasBackground: false,
component: itemComponent component: itemComponent
)) ))
@ -494,8 +488,7 @@ private final class GiftValueSheetContent: CombinedComponent {
color: theme.list.itemAccentColor color: theme.list.itemAccentColor
)), )),
action: { [weak state] in action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(floorPriceString)** is the floor price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.") state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_MinimumPriceInfo(floorPriceString, giftCollectionTitle).string)
} }
).tagged(tag)) ).tagged(tag))
)) ))
@ -504,7 +497,7 @@ private final class GiftValueSheetContent: CombinedComponent {
) )
tableItems.append(.init( tableItems.append(.init(
id: "floorPrice", id: "floorPrice",
title: "Minumum Price", title: strings.Gift_Value_MinimumPrice,
hasBackground: false, hasBackground: false,
component: itemComponent component: itemComponent
)) ))
@ -531,7 +524,7 @@ private final class GiftValueSheetContent: CombinedComponent {
color: theme.list.itemAccentColor color: theme.list.itemAccentColor
)), )),
action: { [weak state] in action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: "**\(averagePriceString)** is the average sale price of \(giftCollectionTitle) on Telegram and Fragment over the past month.") state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_AveragePriceInfo(averagePriceString, giftCollectionTitle).string)
} }
).tagged(tag)) ).tagged(tag))
)) ))
@ -540,7 +533,7 @@ private final class GiftValueSheetContent: CombinedComponent {
) )
tableItems.append(.init( tableItems.append(.init(
id: "averagePrice", id: "averagePrice",
title: "Average Price", title: strings.Gift_Value_AveragePrice,
hasBackground: false, hasBackground: false,
component: itemComponent component: itemComponent
)) ))
@ -587,7 +580,7 @@ private final class GiftValueSheetContent: CombinedComponent {
) )
)), )),
AnyComponentWithIdentity(id: "label", component: AnyComponent( AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Telegram", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)), )),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent( AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
@ -639,7 +632,7 @@ private final class GiftValueSheetContent: CombinedComponent {
) )
)), )),
AnyComponentWithIdentity(id: "label", component: AnyComponent( AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Fragment", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)), )),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent( AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
@ -724,6 +717,7 @@ final class GiftValueSheetComponent: CombinedComponent {
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true, followContentSizeChanges: true,
clipsContent: true, clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState, externalState: sheetExternalState,
animateOut: animateOut, animateOut: animateOut,
onPan: { onPan: {

View File

@ -36,6 +36,7 @@ import StarsBalanceOverlayComponent
import BalanceNeededScreen import BalanceNeededScreen
import GiftItemComponent import GiftItemComponent
import GiftAnimationComponent import GiftAnimationComponent
import ChatThemeScreen
private final class GiftViewSheetContent: CombinedComponent { private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -104,6 +105,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var cachedHiddenImage: (UIImage, PresentationTheme)? var cachedHiddenImage: (UIImage, PresentationTheme)?
var inProgress = false var inProgress = false
var canSkip = false
var testUpgradeAnimation = !"".isEmpty var testUpgradeAnimation = !"".isEmpty
@ -152,7 +154,7 @@ private final class GiftViewSheetContent: CombinedComponent {
super.init() super.init()
if let arguments = subject.arguments { if let arguments = subject.arguments {
if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden { if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden && !arguments.upgradeSeparate {
self.keepOriginalInfo = true self.keepOriginalInfo = true
} }
@ -595,7 +597,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
if let convertToStars = controller?.convertToStars { if let convertToStars = controller?.convertToStars {
convertToStars() convertToStars(reference)
} else { } else {
let _ = (self.context.engine.payments.convertStarGift(reference: reference) let _ = (self.context.engine.payments.convertStarGift(reference: reference)
|> deliverOnMainQueue).startStandalone() |> deliverOnMainQueue).startStandalone()
@ -668,14 +670,34 @@ private final class GiftViewSheetContent: CombinedComponent {
return return
} }
self.isOpeningValue = true self.isOpeningValue = true
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug) let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug)
|> deliverOnMainQueue).start(next: { [weak self] valueInfo in |> deliverOnMainQueue).start(next: { [weak self] valueInfo in
guard let self, let valueInfo else { guard let self else {
return return
} }
self.isOpeningValue = false Queue.mainQueue().after(0.2) {
let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo) self.isOpeningValue = false
controller.push(valueController) }
if let valueInfo {
let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo)
controller.push(valueController)
} else {
guard let controller = self.getController() as? GiftViewScreen else {
return
}
let alertController = textAlertController(
context: self.context,
title: nil,
text: presentationData.strings.Login_UnknownError,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
controller.present(alertController, in: .window(.root))
}
}) })
} }
@ -782,6 +804,83 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.present(shareController, in: .window(.root)) controller.present(shareController, in: .window(.root))
} }
func setAsGiftTheme() {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController, case let .unique(gift) = arguments.gift else {
return
}
let context = self.context
let themePeerId = Promise<EnginePeer.Id?>()
themePeerId.set(
.single(gift.themePeerId)
|> then(
context.engine.payments.getUniqueStarGift(slug: gift.slug)
|> map { gift in
return gift?.themePeerId
}
)
)
let peerController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [.user(.init(isBot: false, isPremium: nil))], hasContactSelector: false, hasCreation: false))
peerController.peerSelected = { [weak peerController, weak navigationController] peer, _ in
if let navigationController {
let proceed = {
let _ = context.engine.themes.setChatWallpaper(peerId: peer.id, wallpaper: nil, forBoth: true).startStandalone()
let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).startStandalone()
peerController?.dismiss()
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
}
let _ = (themePeerId.get()
|> deliverOnMainQueue
|> take(1)).start(next: { [weak navigationController] themePeerId in
if let themePeerId, themePeerId != peer.id {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer else {
proceed()
return
}
let controller = giftThemeTransferAlertController(
context: context,
gift: gift,
previousPeer: peer,
commit: {
proceed()
}
)
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
})
} else {
proceed()
}
})
}
}
self.dismiss(animated: true)
Queue.mainQueue().after(0.4) {
navigationController.pushViewController(peerController)
}
}
func transferGift() { func transferGift() {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else { guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else {
return return
@ -882,7 +981,7 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let self, let controller else { guard let self, let controller else {
return return
} }
let _ = ((controller.updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)) let _ = ((controller.updateResellStars?(reference, nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil))
|> deliverOnMainQueue).startStandalone(error: { error in |> deliverOnMainQueue).startStandalone(error: { error in
}, completed: { [weak self, weak controller] in }, completed: { [weak self, weak controller] in
@ -932,7 +1031,7 @@ private final class GiftViewSheetContent: CombinedComponent {
return return
} }
let _ = ((controller.updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) let _ = ((controller.updateResellStars?(reference, price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price))
|> deliverOnMainQueue).startStandalone(error: { [weak self, weak controller] error in |> deliverOnMainQueue).startStandalone(error: { [weak self, weak controller] error in
guard let self else { guard let self else {
return return
@ -1061,7 +1160,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
let strings = presentationData.strings let strings = presentationData.strings
if let _ = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop = controller.togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop { if let reference = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop = controller.togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop {
items.append(.action(ContextMenuActionItem(text: pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in items.append(.action(ContextMenuActionItem(text: pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self, weak controller] in c?.dismiss(completion: { [weak self, weak controller] in
guard let self, let controller else { guard let self, let controller else {
@ -1069,7 +1168,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
let pinnedToTop = !pinnedToTop let pinnedToTop = !pinnedToTop
if togglePinnedToTop(pinnedToTop) { if togglePinnedToTop(reference, pinnedToTop) {
if pinnedToTop { if pinnedToTop {
controller.dismissAnimated() controller.dismissAnimated()
} else { } else {
@ -1114,6 +1213,16 @@ private final class GiftViewSheetContent: CombinedComponent {
self?.shareGift() self?.shareGift()
}))) })))
if gift.flags.contains(.isThemeAvailable) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_SetAsTheme, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
self?.setAsGiftTheme()
})))
}
if let _ = arguments.transferStars { if let _ = arguments.transferStars {
if case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { if case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
@ -1528,7 +1637,28 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
func skipAnimation() {
guard let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift else {
return
}
self.canSkip = false
self.revealedNumberDigits = "\(uniqueGift.number)".count
self.revealedAttributes.insert(.backdrop)
self.revealedAttributes.insert(.pattern)
self.revealedAttributes.insert(.model)
self.updated(transition: .easeInOut(duration: 0.2))
}
func commitUpgrade() { func commitUpgrade() {
let duration = Double.random(in: 0.85 ..< 2.25)
let firstFraction = Double.random(in: 0.2 ..< 0.4)
let secondFraction = Double.random(in: 0.2 ..< 0.4)
let thirdFraction = 1.0 - firstFraction - secondFraction
let firstDuration = duration * firstFraction
let secondDuration = duration * secondFraction
let thirdDuration = duration * thirdFraction
if self.testUpgradeAnimation, let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift { if self.testUpgradeAnimation, let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift {
self.inProgress = true self.inProgress = true
self.updated() self.updated()
@ -1538,8 +1668,11 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
Queue.mainQueue().after(0.5, { Queue.mainQueue().after(0.5, {
self.inUpgradePreview = false self.canSkip = true
self.updated(transition: .immediate)
self.inProgress = false self.inProgress = false
self.inUpgradePreview = false
self.justUpgraded = true self.justUpgraded = true
self.revealedNumberDigits = -1 self.revealedNumberDigits = -1
@ -1550,20 +1683,24 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updated(transition: .immediate) self.updated(transition: .immediate)
} }
} }
self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(1.2) { Queue.mainQueue().after(firstDuration) {
self.revealedAttributes.insert(.backdrop) self.revealedAttributes.insert(.backdrop)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) { Queue.mainQueue().after(secondDuration) {
self.revealedAttributes.insert(.pattern) self.revealedAttributes.insert(.pattern)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) { Queue.mainQueue().after(thirdDuration) {
self.revealedAttributes.insert(.model) self.revealedAttributes.insert(.model)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.55) {
self.canSkip = false
self.updated(transition: .easeInOut(duration: 0.2))
}
Queue.mainQueue().after(0.6) { Queue.mainQueue().after(0.6) {
if let controller = self.getController() as? GiftViewScreen { if let controller = self.getController() as? GiftViewScreen {
controller.animateSuccess() controller.animateSuccess()
@ -1572,6 +1709,8 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
} }
self.updated(transition: .spring(duration: 0.4))
}) })
return return
} }
@ -1592,8 +1731,11 @@ private final class GiftViewSheetContent: CombinedComponent {
let context = self.context let context = self.context
let upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>) let upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
if let upgradeGift = controller.upgradeGift { if let upgradeGift = controller.upgradeGift {
guard let reference = arguments.reference else {
return
}
upgradeGiftImpl = { formId, keepOriginalInfo in upgradeGiftImpl = { formId, keepOriginalInfo in
return upgradeGift(formId, keepOriginalInfo) return upgradeGift(formId, reference, keepOriginalInfo)
|> afterCompleted { |> afterCompleted {
if formId != nil { if formId != nil {
context.starsContext?.load(force: true) context.starsContext?.load(force: true)
@ -1619,6 +1761,9 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let self, let controller = self.getController() as? GiftViewScreen else { guard let self, let controller = self.getController() as? GiftViewScreen else {
return return
} }
self.canSkip = true
self.updated(transition: .immediate)
self.inProgress = false self.inProgress = false
self.inUpgradePreview = false self.inUpgradePreview = false
@ -1639,18 +1784,23 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
Queue.mainQueue().after(1.2) { Queue.mainQueue().after(firstDuration) {
self.revealedAttributes.insert(.backdrop) self.revealedAttributes.insert(.backdrop)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) { Queue.mainQueue().after(secondDuration) {
self.revealedAttributes.insert(.pattern) self.revealedAttributes.insert(.pattern)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) { Queue.mainQueue().after(thirdDuration) {
self.revealedAttributes.insert(.model) self.revealedAttributes.insert(.model)
self.updated(transition: .immediate) self.updated(transition: .immediate)
Queue.mainQueue().after(0.55) {
self.canSkip = false
self.updated(transition: .easeInOut(duration: 0.2))
}
Queue.mainQueue().after(0.6) { Queue.mainQueue().after(0.6) {
if let controller = self.getController() as? GiftViewScreen { if let controller = self.getController() as? GiftViewScreen {
controller.animateSuccess() controller.animateSuccess()
@ -1661,7 +1811,6 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
self.subject = .profileGift(peerId, result) self.subject = .profileGift(peerId, result)
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4)) self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(0.5) { Queue.mainQueue().after(0.5) {
@ -2267,13 +2416,13 @@ private final class GiftViewSheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0), availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0),
transition: context.transition transition: context.transition
) )
headerComponents.append({
context.add(wearPerks context.add(wearPerks
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0))
.appear(.default(alpha: true)) .appear(.default(alpha: true))
.disappear(.default(alpha: true)) .disappear(.default(alpha: true))
) )
})
originY += wearPerks.size.height originY += wearPerks.size.height
originY += 16.0 originY += 16.0
} else if showUpgradePreview { } else if showUpgradePreview {
@ -3166,8 +3315,8 @@ private final class GiftViewSheetContent: CombinedComponent {
component: PlainButtonComponent( component: PlainButtonComponent(
content: AnyComponent( content: AnyComponent(
HeaderButtonComponent( HeaderButtonComponent(
title: uniqueGift.resellAmounts == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist, title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
iconName: uniqueGift.resellAmounts == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist" iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
) )
), ),
effectAlignment: .center, effectAlignment: .center,
@ -3629,7 +3778,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} else { } else {
resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })
} }
if let resellAmount { if let resellAmount, wearPeerNameChild == nil {
if incoming || ownerPeerId == component.context.account.peerId { if incoming || ownerPeerId == component.context.account.peerId {
let priceButton = priceButton.update( let priceButton = priceButton.update(
component: PlainButtonComponent( component: PlainButtonComponent(
@ -3658,7 +3807,12 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
} }
if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) {
var isChatTheme = false
if let controller = controller() as? GiftViewScreen, controller.openChatTheme != nil {
isChatTheme = true
}
if ((incoming && !converted && !upgraded) || exported || selling || isChatTheme) && (!showUpgradePreview && !showWearPreview) {
let textFont = Font.regular(13.0) let textFont = Font.regular(13.0)
let textColor = theme.list.itemSecondaryTextColor let textColor = theme.list.itemSecondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor let linkColor = theme.actionSheet.controlAccentColor
@ -3672,7 +3826,9 @@ private final class GiftViewSheetContent: CombinedComponent {
var addressToOpen: String? var addressToOpen: String?
var descriptionText: String var descriptionText: String
if let uniqueGift, selling { if isChatTheme {
descriptionText = strings.Gift_View_OpenChatTheme
} else if let uniqueGift, selling {
let ownerName: String let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner { if case let .peerId(peerId) = uniqueGift.owner {
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? "" ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
@ -3731,7 +3887,10 @@ private final class GiftViewSheetContent: CombinedComponent {
}, },
tapAction: { [weak state] attributes, _ in tapAction: { [weak state] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
if let addressToOpen { if isChatTheme, let controller = controller() as? GiftViewScreen {
state?.dismiss(animated: true)
controller.openChatTheme?()
} else if let addressToOpen {
state?.openAddress(addressToOpen) state?.openAddress(addressToOpen)
} else { } else {
state?.updateSavedToProfile(!savedToProfile) state?.updateSavedToProfile(!savedToProfile)
@ -3761,7 +3920,25 @@ private final class GiftViewSheetContent: CombinedComponent {
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
) )
let buttonChild: _UpdatedChildComponent let buttonChild: _UpdatedChildComponent
if showWearPreview, let uniqueGift { if state.canSkip {
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("skip"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_Skip, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
if let state {
state.skipAnimation()
}
}),
availableSize: buttonSize,
transition: context.transition
)
} else if showWearPreview, let uniqueGift {
let buttonContent: AnyComponentWithIdentity<Empty> let buttonContent: AnyComponentWithIdentity<Empty>
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
@ -4081,7 +4258,9 @@ private final class GiftViewSheetContent: CombinedComponent {
isEnabled: true, isEnabled: true,
displaysProgress: state.inProgress, displaysProgress: state.inProgress,
action: { [weak state] in action: { [weak state] in
state?.dismiss(animated: true) if let state {
state.dismiss(animated: true)
}
}), }),
availableSize: buttonSize, availableSize: buttonSize,
transition: context.transition transition: context.transition
@ -4090,7 +4269,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size) let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size)
var buttonAlpha: CGFloat = 1.0 var buttonAlpha: CGFloat = 1.0
if let nextGiftToUpgrade = state.nextGiftToUpgrade, case let .generic(gift) = nextGiftToUpgrade.gift { if let nextGiftToUpgrade = state.nextGiftToUpgrade, case let .generic(gift) = nextGiftToUpgrade.gift, !state.canSkip {
buttonAlpha = 0.0 buttonAlpha = 0.0
let upgradeNextButton = upgradeNextButton.update( let upgradeNextButton = upgradeNextButton.update(
@ -4098,7 +4277,7 @@ private final class GiftViewSheetContent: CombinedComponent {
content: AnyComponent( content: AnyComponent(
HStack([ HStack([
AnyComponentWithIdentity(id: "label", component: AnyComponent( AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Upgrade Next Gift", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_UpgradeNext, font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)), )),
AnyComponentWithIdentity(id: "icon", component: AnyComponent( AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent( GiftItemComponent(
@ -4278,12 +4457,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String) case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift) case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?)? { var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool)? {
switch self { switch self {
case let .message(message): case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
switch action.action { switch action.action {
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, _): case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate):
var reference: StarGiftReference var reference: StarGiftReference
if let peerId, let giftMessageId { if let peerId, let giftMessageId {
reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId)) reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId))
@ -4292,7 +4471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else { } else {
reference = .message(messageId: message.id) reference = .message(messageId: message.id)
} }
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash) return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate): case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
var reference: StarGiftReference var reference: StarGiftReference
if let peerId, let savedId { if let peerId, let savedId {
@ -4317,13 +4496,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift { if case let .unique(uniqueGift) = gift {
resellAmounts = uniqueGift.resellAmounts resellAmounts = uniqueGift.resellAmounts
} }
return (message.id.peerId, senderId ?? message.author?.id, 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) return (message.id.peerId, senderId ?? message.author?.id, 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)
default: default:
return nil return nil
} }
} }
case let .uniqueGift(gift, _), let .wearPreview(gift): case let .uniqueGift(gift, _), let .wearPreview(gift):
return (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) return (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)
case let .profileGift(peerId, gift): case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id? var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference { if case let .message(messageIdValue) = gift.reference {
@ -4333,7 +4512,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift { if case let .unique(uniqueGift) = gift.gift {
resellAmounts = uniqueGift.resellAmounts resellAmounts = uniqueGift.resellAmounts
} }
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash) return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate)
case .soldOutGift: case .soldOutGift:
return nil return nil
case .upgradePreview: case .upgradePreview:
@ -4374,13 +4553,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
fileprivate let balanceOverlay = ComponentView<Empty>() fileprivate let balanceOverlay = ComponentView<Empty>()
fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)?
fileprivate let convertToStars: (() -> Void)? fileprivate let convertToStars: ((StarGiftReference) -> Void)?
fileprivate let transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? fileprivate let transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)?
fileprivate let upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? fileprivate let upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
fileprivate let buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)? fileprivate let buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)?
fileprivate let updateResellStars: ((CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)? fileprivate let updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)?
fileprivate let togglePinnedToTop: ((Bool) -> Bool)? fileprivate let togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)?
fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)? fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)?
fileprivate let openChatTheme: (() -> Void)?
public var disposed: () -> Void = {} public var disposed: () -> Void = {}
@ -4391,13 +4571,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
index: Int? = nil, index: Int? = nil,
forceDark: Bool = false, forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil, updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil, convertToStars: ((StarGiftReference) -> Void)? = nil,
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil, transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil, upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)? = nil, buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)? = nil,
updateResellStars: ((CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil, updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil, togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil shareStory: ((StarGift.UniqueGift) -> Void)? = nil,
openChatTheme: (() -> Void)? = nil
) { ) {
self.context = context self.context = context
self.subject = subject self.subject = subject
@ -4410,6 +4591,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.updateResellStars = updateResellStars self.updateResellStars = updateResellStars
self.togglePinnedToTop = togglePinnedToTop self.togglePinnedToTop = togglePinnedToTop
self.shareStory = shareStory self.shareStory = shareStory
self.openChatTheme = openChatTheme
if case let .unique(gift) = subject.arguments?.gift, gift.resellForTonOnly { if case let .unique(gift) = subject.arguments?.gift, gift.resellForTonOnly {
self.balanceCurrency = .ton self.balanceCurrency = .ton
@ -4453,8 +4635,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false self.automaticallyControlPresentationContextLayout = false
if "".isEmpty { if let gift = subject.arguments?.gift, case .generic = gift {
let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable]) let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable], limit: 50)
self.upgradableDisposable = (upgradableGiftsContext.state self.upgradableDisposable = (upgradableGiftsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in |> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else { guard let self else {
@ -4529,7 +4711,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveModalDismiss = true
if let arguments = self.subject.arguments, let _ = self.subject.arguments?.resellAmounts { if let arguments = self.subject.arguments, let resellAmounts = self.subject.arguments?.resellAmounts, !resellAmounts.isEmpty {
if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner { if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner {
} else { } else {
self.showBalance = true self.showBalance = true

View File

@ -127,7 +127,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
})) }))
} }
let presence: EnginePeer.Presence var presence: EnginePeer.Presence
if member.peer.id == context.account.peerId { if member.peer.id == context.account.peerId {
presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0) presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0)
} else if let value = member.presence { } else if let value = member.presence {
@ -136,6 +136,17 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0) presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0)
} }
var status: ContactsPeerItemStatus = .presence(presence, presentationData.dateTimeFormat)
if let user = member.peer as? TelegramUser, let botInfo = user.botInfo {
let botStatus: String
if botInfo.flags.contains(.hasAccessToChatHistory) {
botStatus = presentationData.strings.Bot_GroupStatusReadsHistory
} else {
botStatus = presentationData.strings.Bot_GroupStatusDoesNotReadHistory
}
status = .custom(string: NSAttributedString(string: botStatus, font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 14.0 / 17.0)), textColor: presentationData.theme.list.itemSecondaryTextColor), multiline: false, isActive: false, icon: nil)
}
return ContactsPeerItem( return ContactsPeerItem(
presentationData: ItemListPresentationData(presentationData), presentationData: ItemListPresentationData(presentationData),
style: .plain, style: .plain,
@ -145,7 +156,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
context: context, context: context,
peerMode: .memberList, peerMode: .memberList,
peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)), peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)),
status: .presence(presence, presentationData.dateTimeFormat), status: status,
rightLabelText: label, rightLabelText: label,
enabled: true, enabled: true,
selection: .none, selection: .none,

View File

@ -589,14 +589,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
var currentSavedMusic: TelegramMediaFile? var currentSavedMusic: TelegramMediaFile?
if !self.isSettings, let screenData { if let peer, peer.id != self.context.account.peerId || self.isMyProfile, let screenData {
if let savedMusicState = screenData.savedMusicState { if let savedMusicState = screenData.savedMusicState {
currentSavedMusic = savedMusicState.files.first currentSavedMusic = savedMusicState.files.first
} else if let cachedUserData = screenData.cachedData as? CachedUserData { } else if let cachedUserData = screenData.cachedData as? CachedUserData {
currentSavedMusic = cachedUserData.savedMusic currentSavedMusic = cachedUserData.savedMusic
} }
} }
let musicHeight: CGFloat = hasBackground ? 24.0 : 16.0 let musicHeight: CGFloat = hasBackground || self.isAvatarExpanded ? 24.0 : 16.0
let bottomInset: CGFloat = currentSavedMusic != nil ? musicHeight : 0.0 let bottomInset: CGFloat = currentSavedMusic != nil ? musicHeight : 0.0
let isLandscape = containerInset > 16.0 let isLandscape = containerInset > 16.0
@ -2640,7 +2640,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
return musicBackground return musicBackground
}() }()
musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: musicHeight))) musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - 24.0 - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: 24.0)))
if let _ = self.navigationTransition { if let _ = self.navigationTransition {
transition.updateAlpha(layer: musicBackground.layer, alpha: 1.0 - transitionFraction) transition.updateAlpha(layer: musicBackground.layer, alpha: 1.0 - transitionFraction)
@ -2698,7 +2698,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
environment: {}, environment: {},
containerSize: CGSize(width: backgroundFrame.width, height: musicHeight) containerSize: CGSize(width: backgroundFrame.width, height: musicHeight)
) )
let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: (apparentBackgroundHeight - backgroundHeight) + backgroundHeight - musicHeight - (hasBackground ? 0.0 : 4.0)), size: musicSize) let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: (apparentBackgroundHeight - backgroundHeight) + backgroundHeight - musicHeight - (hasBackground || self.isAvatarExpanded ? 0.0 : 4.0)), size: musicSize)
if let musicView = music.view { if let musicView = music.view {
if musicView.superview == nil { if musicView.superview == nil {
self.regularContentNode.view.addSubview(musicView) self.regularContentNode.view.addSubview(musicView)

View File

@ -1163,9 +1163,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
return return
} }
//TODO:localize
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Set as Main Tab", icon: { theme in items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
guard let self else { guard let self else {
@ -1183,7 +1182,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
guard let self else { guard let self else {
return return
} }
let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: "Tab order changed.", cancel: nil, destructive: false), action: { _ in return true }) let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab_Succeed, cancel: nil, destructive: false), action: { _ in return true })
self.parentController?.present(controller, in: .current) self.parentController?.present(controller, in: .current)
}) })
} }

View File

@ -5012,8 +5012,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added) profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added)
}, },
convertToStars: { [weak profileGifts] in convertToStars: { [weak profileGifts] reference in
guard let profileGifts, let reference = gift.reference else { guard let profileGifts else {
return return
} }
profileGifts.convertStarGift(reference: reference) profileGifts.convertStarGift(reference: reference)
@ -5024,8 +5024,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
return profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) return profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
}, },
upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in upgradeGift: { [weak profileGifts] formId, reference, keepOriginalInfo in
guard let profileGifts, let reference = gift.reference else { guard let profileGifts else {
return .never() return .never()
} }
return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)

View File

@ -611,8 +611,8 @@ final class GiftsListView: UIView {
} }
self.profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added) self.profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added)
}, },
convertToStars: { [weak self] in convertToStars: { [weak self] reference in
guard let self, let reference = product.reference else { guard let self else {
return return
} }
self.profileGifts.convertStarGift(reference: reference) self.profileGifts.convertStarGift(reference: reference)
@ -623,8 +623,8 @@ final class GiftsListView: UIView {
} }
return self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) return self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
}, },
upgradeGift: { [weak self] formId, keepOriginalInfo in upgradeGift: { [weak self] formId, reference, keepOriginalInfo in
guard let self, let reference = product.reference else { guard let self else {
return .never() return .never()
} }
return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
@ -635,36 +635,34 @@ final class GiftsListView: UIView {
} }
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId, price: price) return self.profileGifts.buyStarGift(slug: slug, peerId: peerId, price: price)
}, },
updateResellStars: { [weak self] price in updateResellStars: { [weak self] reference, price in
guard let self, let reference = product.reference else { guard let self else {
return .never() return .never()
} }
return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
}, },
togglePinnedToTop: { [weak self] pinnedToTop in togglePinnedToTop: { [weak self] reference, pinnedToTop in
guard let self else { guard let self else {
return false return false
} }
if let reference = product.reference { if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount { self.displayUnpinScreen?(product, {
self.displayUnpinScreen?(product, { dismissImpl?()
dismissImpl?() })
}) return false
return false }
} self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
var title = "" var title = ""
if case let .unique(uniqueGift) = product.gift { if case let .unique(uniqueGift) = product.gift {
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))" title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))"
} }
if pinnedToTop { if pinnedToTop {
Queue.mainQueue().after(0.35) { Queue.mainQueue().after(0.35) {
let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
let toastText = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Text let toastText = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Text
self.parentController?.present(UndoOverlayController(presentationData: params.presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) self.parentController?.present(UndoOverlayController(presentationData: params.presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
} }
} }
return true return true
@ -996,7 +994,7 @@ final class GiftsListView: UIView {
fadeTransition.setAlpha(view: self.emptyResultsClippingView, alpha: visibleHeight < 300.0 ? 0.0 : 1.0) fadeTransition.setAlpha(view: self.emptyResultsClippingView, alpha: visibleHeight < 300.0 ? 0.0 : 1.0)
if self.peerId == self.context.account.peerId, !self.canSelect && !self.filteredResultsAreEmpty && self.profileGifts.collectionId == nil { if self.peerId == self.context.account.peerId, !self.canSelect && !self.filteredResultsAreEmpty && self.profileGifts.collectionId == nil && self.emptyResultsClippingView.isHidden {
let footerText: ComponentView<Empty> let footerText: ComponentView<Empty>
if let current = self.footerText { if let current = self.footerText {
footerText = current footerText = current
@ -1024,6 +1022,13 @@ final class GiftsListView: UIView {
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - footerTextSize.width) / 2.0), y: contentHeight), size: footerTextSize)) transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - footerTextSize.width) / 2.0), y: contentHeight), size: footerTextSize))
} }
contentHeight += footerTextSize.height contentHeight += footerTextSize.height
} else if let footerText = self.footerText {
self.footerText = nil
if let view = footerText.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
} }
return contentHeight return contentHeight

View File

@ -90,7 +90,6 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let tabSelector = ComponentView<Empty>() private let tabSelector = ComponentView<Empty>()
public private(set) var currentCollection: GiftCollection = .all public private(set) var currentCollection: GiftCollection = .all
private var footerText: ComponentView<Empty>?
private var panelBackground: NavigationBackgroundNode? private var panelBackground: NavigationBackgroundNode?
private var panelSeparator: ASDisplayNode? private var panelSeparator: ASDisplayNode?
private var panelButton: ComponentView<Empty>? private var panelButton: ComponentView<Empty>?

View File

@ -377,24 +377,46 @@ private final class ProfileLevelInfoScreenComponent: Component {
descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string
} }
//TODO:localize
var titleItems: [AnimatedTextComponent.Item] = [] var titleItems: [AnimatedTextComponent.Item] = []
let ratingTitle = environment.strings.ProfileLevelInfo_RatingTitle
let futureTitle = environment.strings.ProfileLevelInfo_FutureRatingTitle
if self.isPreviewingPendingRating { if self.isPreviewingPendingRating {
titleItems.append(AnimatedTextComponent.Item( if let range = futureTitle.range(of: ratingTitle) {
id: AnyHashable(0), if !futureTitle[..<range.lowerBound].isEmpty {
isUnbreakable: false, titleItems.append(AnimatedTextComponent.Item(
content: .text("Future ") id: AnyHashable(0),
)) isUnbreakable: false,
titleItems.append(AnimatedTextComponent.Item( content: .text(String(futureTitle[..<range.lowerBound]))
id: AnyHashable(1), ))
isUnbreakable: true, }
content: .text("Rating")
)) titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1),
isUnbreakable: true,
content: .text(ratingTitle)
))
if !futureTitle[range.upperBound...].isEmpty {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(2),
isUnbreakable: false,
content: .text(String(futureTitle[range.upperBound...]))
))
}
} else {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(0),
isUnbreakable: true,
content: .text(futureTitle)
))
}
} else { } else {
titleItems.append(AnimatedTextComponent.Item( titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1), id: AnyHashable(1),
isUnbreakable: true, isUnbreakable: true,
content: .text("Rating") content: .text(ratingTitle)
)) ))
} }

View File

@ -465,7 +465,9 @@ final class UserAppearanceScreenComponent: Component {
resellForTonOnly: false, resellForTonOnly: false,
releasedBy: nil, releasedBy: nil,
valueAmount: nil, valueAmount: nil,
valueCurrency: nil valueCurrency: nil,
flags: [],
themePeerId: nil
) )
signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate) signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate)
} else { } else {

View File

@ -273,9 +273,9 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white)
} }
imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true) imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in |> mapToSignal { generatorAndRects -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value = value { if let (generator, _) = generatorAndRects {
return .single(value) return .single(generator)
} else { } else {
return .complete() return .complete()
} }

View File

@ -673,14 +673,13 @@ final class StarsTransactionsScreenComponent: Component {
let withdrawAvailable = (self.revenueState?.balances.overallRevenue.amount.value ?? 0) > 0 let withdrawAvailable = (self.revenueState?.balances.overallRevenue.amount.value ?? 0) > 0
if component.starsContext.ton { if component.starsContext.ton {
//TODO:localize
let proceedsSize = self.proceedsView.update( let proceedsSize = self.proceedsView.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(ListSectionComponent( component: AnyComponent(ListSectionComponent(
theme: environment.theme, theme: environment.theme,
header: AnyComponent(MultilineTextComponent( header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "Proceeds Overview".uppercased(), string: environment.strings.Ton_ProceedsOverview.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor textColor: environment.theme.list.freeTextColor
)), )),
@ -691,14 +690,14 @@ final class StarsTransactionsScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent( AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme, theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat, dateTimeFormat: environment.dateTimeFormat,
title: "Balance Available to Withdraw", title: environment.strings.Ton_AvailableBalance,
value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .ton), value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0 rate: self.revenueState?.usdRate ?? 0.0
))), ))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent( AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme, theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat, dateTimeFormat: environment.dateTimeFormat,
title: "Total Lifetime Proceeds", title: environment.strings.Ton_LifetimeProceeds,
value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .ton), value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0 rate: self.revenueState?.usdRate ?? 0.0
))) )))
@ -725,7 +724,7 @@ final class StarsTransactionsScreenComponent: Component {
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
}) })
let balanceInfoRawString = "Collect your TON using Fragment. [Learn More >]()" let balanceInfoRawString = environment.strings.Ton_WithdrawViaFragment_Info
let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceInfoRawString, attributes: termsMarkdownAttributes, textAlignment: .natural)) let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceInfoRawString, attributes: termsMarkdownAttributes, textAlignment: .natural))
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
@ -754,7 +753,7 @@ final class StarsTransactionsScreenComponent: Component {
}, },
tapAction: { [weak self] attributes, _ in tapAction: { [weak self] attributes, _ in
if let controller = self?.controller?() as? StarsTransactionsScreen, let navigationController = controller.navigationController as? NavigationController { if let controller = self?.controller?() as? StarsTransactionsScreen, let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment_Info_URL : environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
} }
} }
)) : nil, )) : nil,
@ -766,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component {
count: self.starsState?.balance ?? StarsAmount.zero, count: self.starsState?.balance ?? StarsAmount.zero,
currency: component.starsContext.ton ? .ton : .stars, currency: component.starsContext.ton ? .ton : .stars,
rate: nil, rate: nil,
actionTitle: component.starsContext.ton ? "Withdraw via Fragment" : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy), actionTitle: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
actionAvailable: (!premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled), actionAvailable: (!premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled),
actionIsEnabled: true, actionIsEnabled: true,
actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "rotate_18.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -101,9 +101,22 @@ func openWebAppImpl(
} }
} }
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id)) var botPeer = botPeer
|> deliverOnMainQueue).start(next: { appSettings in if case let .inline(bot) = source {
let openWebView = { [weak parentController] in botPeer = bot
}
let _ = combineLatest(queue: Queue.mainQueue(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id)),
ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id),
context.engine.messages.attachMenuBots(),
context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<AttachMenuBot?, NoError> in
return .single(nil)
}
).start(next: { appSettings, noticed, attachMenuBots, attachMenuBot in
let openWebView: (Bool) -> Void = { [weak parentController] justInstalled in
guard let parentController else { guard let parentController else {
return return
} }
@ -305,6 +318,11 @@ func openWebAppImpl(
presentImpl = { [weak controller] c, a in presentImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a) controller?.present(c, in: .window(.root), with: a)
} }
if justInstalled {
let content: UndoOverlayContent = .succeed(text: presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil)
controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
}
}, error: { [weak parentController] error in }, error: { [weak parentController] error in
if let parentController { if let parentController {
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
@ -314,25 +332,37 @@ func openWebAppImpl(
} }
} }
if skipTermsOfService { var isAttachMenuBotInstalled: Bool?
openWebView() if let _ = attachMenuBot {
} else { if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) {
var botPeer = botPeer isAttachMenuBotInstalled = true
if case let .inline(bot) = source { } else {
botPeer = bot isAttachMenuBotInstalled = false
} }
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) }
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in
guard let parentController else {
return
}
if value { if !noticed || attachMenuBot?.flags.contains(.notActivated) == true || isAttachMenuBotInstalled == false {
openWebView() if let isAttachMenuBotInstalled, let attachMenuBot {
if !isAttachMenuBotInstalled {
let controller = webAppTermsAlertController(context: context, updatedPresentationData: updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: {
openWebView(true)
})
})
parentController.present(controller, in: .window(.root))
} else {
openWebView(false)
}
} else {
if skipTermsOfService {
openWebView(false)
} else { } else {
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
openWebView() openWebView(false)
}, showMore: nil, openTerms: { }, showMore: nil, openTerms: {
if let navigationController = parentController.navigationController as? NavigationController { if let navigationController = parentController.navigationController as? NavigationController {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
@ -340,7 +370,9 @@ func openWebAppImpl(
}) })
parentController.present(controller, in: .window(.root)) parentController.present(controller, in: .window(.root))
} }
}) }
} else {
openWebView(false)
} }
}) })
} }

View File

@ -123,6 +123,7 @@ import ChatEmptyNode
import ChatMediaInputStickerGridItem import ChatMediaInputStickerGridItem
import AdsInfoScreen import AdsInfoScreen
import Photos import Photos
import ChatThemeScreen
extension ChatControllerImpl { extension ChatControllerImpl {
public func presentThemeSelection() { public func presentThemeSelection() {
@ -182,15 +183,15 @@ extension ChatControllerImpl {
previewTheme: { [weak self] chatTheme, dark in previewTheme: { [weak self] chatTheme, dark in
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentCrossfadeSnapshot() strongSelf.presentCrossfadeSnapshot()
strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), dark))) strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme, dark)))
} }
}, },
changeWallpaper: { [weak self] in changeWallpaper: { [weak self] in
guard let strongSelf = self, let peerId else { guard let self, let peerId else {
return return
} }
if let themeController = strongSelf.themeScreen { if let themeController = self.themeScreen {
strongSelf.themeScreen = nil self.themeScreen = nil
themeController.dimTapped() themeController.dimTapped()
} }
let dismissControllers = { [weak self] in let dismissControllers = { [weak self] in
@ -206,69 +207,67 @@ extension ChatControllerImpl {
} }
var openWallpaperPickerImpl: ((Bool) -> Void)? var openWallpaperPickerImpl: ((Bool) -> Void)?
let openWallpaperPicker = { [weak self] animateAppearance in let openWallpaperPicker = { [weak self] animateAppearance in
guard let strongSelf = self else { guard let self else {
return return
} }
let controller = wallpaperMediaPickerController( let controller = wallpaperMediaPickerController(
context: strongSelf.context, context: context,
updatedPresentationData: strongSelf.updatedPresentationData, updatedPresentationData: self.updatedPresentationData,
peer: EnginePeer(peer), peer: EnginePeer(peer),
animateAppearance: animateAppearance, animateAppearance: animateAppearance,
completion: { [weak self] _, result in completion: { [weak self] _, result in
guard let strongSelf = self, let asset = result as? PHAsset else { guard let self, let asset = result as? PHAsset else {
return return
} }
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) let controller = WallpaperGalleryController(context: context, source: .asset(asset), mode: .peer(EnginePeer(peer), false))
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in controller.apply = { wallpaper, options, editedImage, cropRect, brightness, forBoth in
if let strongSelf = self { uploadCustomPeerWallpaper(context: context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: {
uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: { Queue.mainQueue().after(0.3, {
Queue.mainQueue().after(0.3, { dismissControllers()
dismissControllers()
})
}) })
} })
} }
strongSelf.push(controller) self.push(controller)
}, },
openColors: { [weak self] in openColors: { [weak self] in
guard let strongSelf = self else { guard let self else {
return return
} }
let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in let controller = standaloneColorPickerController(context: context, peer: EnginePeer(peer), push: { [weak self] controller in
if let strongSelf = self { if let self {
strongSelf.push(controller) self.push(controller)
} }
}, openGallery: { }, openGallery: {
openWallpaperPickerImpl?(false) openWallpaperPickerImpl?(false)
}) })
controller.navigationPresentation = .flatModal controller.navigationPresentation = .flatModal
strongSelf.push(controller) self.push(controller)
} }
) )
controller.navigationPresentation = .flatModal controller.navigationPresentation = .flatModal
strongSelf.push(controller) self.push(controller)
} }
openWallpaperPickerImpl = openWallpaperPicker openWallpaperPickerImpl = openWallpaperPicker
openWallpaperPicker(true) openWallpaperPicker(true)
}, },
resetWallpaper: { [weak self] in resetWallpaper: { [weak self] in
guard let strongSelf = self, let peerId else { guard let self, let peerId else {
return return
} }
let _ = strongSelf.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() let _ = self.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone()
}, },
completion: { [weak self] chatTheme in completion: { [weak self] chatTheme in
guard let strongSelf = self, let peerId else { guard let self, let peerId else {
return return
} }
if canResetWallpaper && chatTheme != nil { if canResetWallpaper && chatTheme != nil {
let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: true).startStandalone()
} }
strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), nil))) strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), nil)))
let _ = context.engine.themes.setChatTheme(peerId: peerId, chatTheme: chatTheme ?? .emoticon("")).startStandalone(completed: { [weak self] in let _ = context.engine.themes.setChatTheme(peerId: peerId, chatTheme: chatTheme ?? .emoticon("")).startStandalone(completed: { [weak self] in
if let strongSelf = self { if let self {
strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil))) self.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil)))
} }
}) })
} }

View File

@ -141,6 +141,7 @@ import SuggestedPostApproveAlert
import AVFoundation import AVFoundation
import BalanceNeededScreen import BalanceNeededScreen
import FaceScanScreen import FaceScanScreen
import ChatThemeScreen
public final class ChatControllerOverlayPresentationData { public final class ChatControllerOverlayPresentationData {
public let expandData: (ASDisplayNode?, () -> Void) public let expandData: (ASDisplayNode?, () -> Void)
@ -1110,8 +1111,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(BotReceiptController(context: self.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) self.present(BotReceiptController(context: self.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} }
return true return true
case .setChatTheme: case let .setChatTheme(chatTheme):
self.presentThemeSelection() switch chatTheme {
case .emoticon:
self.presentThemeSelection()
case let .gift(gift, _):
if case let .unique(uniqueGift) = gift {
let controller = self.context.sharedContext.makeGiftViewScreen(context: self.context, gift: uniqueGift, 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)
}
}
}, openChatTheme: { [weak self] in
if let self {
Queue.mainQueue().after(0.15) {
self.presentThemeSelection()
}
}
}, dismissed: nil)
self.push(controller)
}
}
return true return true
case let .setChatWallpaper(wallpaper, _): case let .setChatWallpaper(wallpaper, _):
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
@ -5808,10 +5830,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
} }
} }
case let .gift(gift, wallpaper): case .gift:
let _ = gift if let darkAppearancePreview = darkAppearancePreview {
let _ = wallpaper useDarkAppearance = darkAppearancePreview
//TODO:release }
if let theme = makePresentationTheme(chatTheme: chatTheme, dark: useDarkAppearance) {
theme.forceSync = true
presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper)
Queue.mainQueue().after(1.0, {
theme.forceSync = false
})
}
} }
} else if let darkAppearancePreview = darkAppearancePreview { } else if let darkAppearancePreview = darkAppearancePreview {
useDarkAppearance = darkAppearancePreview useDarkAppearance = darkAppearancePreview

View File

@ -46,6 +46,7 @@ import ComponentFlow
import ChatEmptyNode import ChatEmptyNode
import SpaceWarpView import SpaceWarpView
import ChatSideTopicsPanel import ChatSideTopicsPanel
import ChatThemeScreen
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode let itemNode: OverlayMediaItemNode
@ -3582,7 +3583,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let themeUpdated = presentationReadyUpdated || (self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme) let themeUpdated = presentationReadyUpdated || (self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme)
self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper, animated: true) self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper, starGift: chatPresentationInterfaceState.theme.starGift, animated: true)
self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8)
if self.pendingSwitchToChatLocation == nil { if self.pendingSwitchToChatLocation == nil {

View File

@ -2053,7 +2053,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
var isSuspiciousPeer = false var isSuspiciousPeer = false
if let cachedUserData = data.cachedData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.flags.contains(.canBlock) { if let cachedUserData = data.cachedData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.flags.contains(.canBlock) || peerStatusSettings.flags.contains(.canReport) {
isSuspiciousPeer = true isSuspiciousPeer = true
} }
@ -2309,7 +2309,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll) let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll)
var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: processedView.filteredEntries.count > 1 && canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition)
if disableAnimations { if disableAnimations {
mappedTransition.options.remove(.AnimateInsertion) mappedTransition.options.remove(.AnimateInsertion)

View File

@ -1481,7 +1481,7 @@ func openResolvedUrlImpl(
navigationController?.pushViewController(controller) navigationController?.pushViewController(controller)
} }
} }
}, dismissed: { }, openChatTheme: nil, dismissed: {
dismissedImpl?() dismissedImpl?()
}) })
navigationController?.pushViewController(controller) navigationController?.pushViewController(controller)

View File

@ -57,6 +57,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
private var savedIdsPromise = Promise<Set<Int64>?>() private var savedIdsPromise = Promise<Set<Int64>?>()
private var savedIds: Set<Int64>? private var savedIds: Set<Int64>?
private var copyProtectionEnabled = false
init( init(
context: AccountContext, context: AccountContext,
chatLocation: ChatLocation, chatLocation: ChatLocation,
@ -275,13 +277,17 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
self.historyNode.endedInteractiveDragging = { [weak self] _ in self.historyNode.endedInteractiveDragging = { [weak self] _ in
guard let strongSelf = self else { guard let self else {
return return
} }
switch strongSelf.historyNode.visibleContentOffset() { switch self.historyNode.visibleContentOffset() {
case let .known(value): case let .known(value):
if value <= -10.0 { if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
strongSelf.requestDismiss()
} else {
if value <= -10.0 {
self.requestDismiss()
}
} }
default: default:
break break
@ -388,14 +394,25 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
}) })
self.savedIdsDisposable = (context.engine.peers.savedMusicIds() let copyProtectionEnabled: Signal<Bool, NoError>
|> deliverOnMainQueue).start(next: { [weak self] savedIds in if case let .peer(peerId) = self.chatLocation {
copyProtectionEnabled = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CopyProtectionEnabled(id: peerId))
} else {
copyProtectionEnabled = .single(false)
}
self.savedIdsDisposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.savedMusicIds(),
copyProtectionEnabled
).start(next: { [weak self] savedIds, copyProtectionEnabled in
guard let self else { guard let self else {
return return
} }
let isFirstTime = self.savedIds == nil let isFirstTime = self.savedIds == nil
self.savedIds = savedIds self.savedIds = savedIds
self.savedIdsPromise.set(.single(savedIds)) self.savedIdsPromise.set(.single(savedIds))
self.copyProtectionEnabled = copyProtectionEnabled
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring) let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring)
self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition) self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition)
@ -439,11 +456,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
panRecognizer.delaysTouchesBegan = false panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true panRecognizer.cancelsTouchesInView = true
panRecognizer.shouldBegin = { [weak self] point in panRecognizer.shouldBegin = { [weak self] point in
guard let strongSelf = self else { guard let self else {
return false return false
} }
if strongSelf.controlsNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.controlsNode.view)) { if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) {
if strongSelf.controlsNode.frame.maxY <= strongSelf.historyNode.frame.minY { if self.controlsNode.frame.maxY <= self.historyNode.frame.minY {
return true return true
} }
} }
@ -494,8 +511,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
return .single(true) return .single(true)
} }
}) })
self.historyNode.useMainQueueTransactions = false
self.historyNode.autoScrollWhenReordering = false self.historyNode.autoScrollWhenReordering = false
self.historyNode.didEndScrollingWithOverscroll = { [weak self] in
guard let self else {
return
}
self.requestDismiss()
}
} }
@ -557,19 +579,18 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
func addToSavedMusic(file: FileMediaReference) { func addToSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips() self.dismissAllTooltips()
var actionText: String? = "View" var actionText: String? = self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View
if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId { if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId {
actionText = nil actionText = nil
} }
//TODO:localize
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: self.presentationData, presentationData: self.presentationData,
content: .universalImage( content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil, size: nil,
title: nil, title: nil,
text: "Audio added to your profile.", text: self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile,
customUndoText: actionText, customUndoText: actionText,
timeout: 3.0 timeout: 3.0
), ),
@ -607,14 +628,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
func removeFromSavedMusic(file: FileMediaReference) { func removeFromSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips() self.dismissAllTooltips()
//TODO:localize
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: self.presentationData, presentationData: self.presentationData,
content: .universalImage( content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil, size: nil,
title: nil, title: nil,
text: "Audio removed from your profile.", text: self.presentationData.strings.MediaPlayer_SavedMusic_RemovedFromProfile,
customUndoText: nil, customUndoText: nil,
timeout: 3.0 timeout: 3.0
), ),
@ -638,6 +658,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
private var isSaved: Bool? { private var isSaved: Bool? {
if self .copyProtectionEnabled {
return nil
}
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
return nil
}
guard let fileReference = self.controlsNode.currentFileReference else { guard let fileReference = self.controlsNode.currentFileReference else {
return nil return nil
} }
@ -672,7 +698,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5))
var itemOffsetInsets = insets var itemOffsetInsets = insets
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
itemOffsetInsets.top = 0.0 itemOffsetInsets.top = 0.0
itemOffsetInsets.bottom = 0.0 itemOffsetInsets.bottom = 0.0
@ -981,9 +1006,16 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5))
var itemOffsetInsets = insets
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
itemOffsetInsets.top = 0.0
itemOffsetInsets.bottom = 0.0
insets = itemOffsetInsets
}
self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: 0.0, curve: .Default(duration: nil))
self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets)
self.historyNode.recursivelyEnsureDisplaySynchronously(true) self.historyNode.recursivelyEnsureDisplaySynchronously(true)
@ -1010,10 +1042,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
//TODO:localize
if canSaveToProfile || canSaveToSavedMessages { if canSaveToProfile || canSaveToSavedMessages {
items.append( items.append(
.action(ContextMenuActionItem(text: "Save to...", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
if let self { if let self {
var subActions: [ContextMenuItem] = [] var subActions: [ContextMenuItem] = []
subActions.append( subActions.append(
@ -1025,7 +1056,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToProfile { if canSaveToProfile {
subActions.append( subActions.append(
.action(ContextMenuActionItem(text: "Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
if let self { if let self {
@ -1037,7 +1068,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToSavedMessages { if canSaveToSavedMessages {
subActions.append( subActions.append(
.action(ContextMenuActionItem(text: "Saved Messages", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
if let self { if let self {
@ -1048,7 +1079,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
subActions.append( subActions.append(
.action(ContextMenuActionItem(text: "Files", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Files, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default) f(.default)
if let self { if let self {
@ -1072,7 +1103,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil
subActions.append( subActions.append(
.action(ContextMenuActionItem(text: "Choose where you want this audio to be saved.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction))
) )
c?.pushItems(items: .single(ContextController.Items(content: .list(subActions)))) c?.pushItems(items: .single(ContextController.Items(content: .list(subActions))))
@ -1080,7 +1111,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
})) }))
) )
} else { } else {
items.append(.action(ContextMenuActionItem(text: "Save to Files", icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveToFiles, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.default) f(.default)
@ -1112,7 +1143,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
addedSeparator = true addedSeparator = true
} }
items.append( items.append(
.action(ContextMenuActionItem(text: "Show in Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_ShowInChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
guard let self else { guard let self else {
@ -1125,7 +1156,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
} }
// items.append( // items.append(
// .action(ContextMenuActionItem(text: "Forward", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in // .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_Forward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default) // f(.default)
// //
// if let _ = self { // if let _ = self {
@ -1162,9 +1193,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
items.append(.separator) items.append(.separator)
addedSeparator = true addedSeparator = true
} }
var actionTitle = "Delete" var actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Delete
if case .custom = self.source { if case .custom = self.source {
actionTitle = "Remove" actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Remove
} }
items.append( items.append(
.action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in .action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in

View File

@ -1026,13 +1026,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.separatorNode.isHidden = hasSectionHeader self.separatorNode.isHidden = hasSectionHeader
if hasSectionHeader { if hasSectionHeader {
//TODO:localize
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0
var sectionTitle = "AUDIO IN THIS CHAT" var sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_ThisChat
if let peerName = self.peerName { if let peerName = self.peerName {
sectionTitle = "\(peerName.uppercased())'S PLAYLIST" sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusic(peerName.uppercased()).string
} else if case .custom = self.source { } else if case .custom = self.source {
sectionTitle = "YOUR PLAYLIST" sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusicYou
} }
let sectionTitleSize = self.sectionTitle.update( let sectionTitleSize = self.sectionTitle.update(
transition: .immediate, transition: .immediate,
@ -1096,7 +1095,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
}) })
let attributedString = parseMarkdownIntoAttributedString("This audio is visible on your profile. [Remove >]()", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString let attributedString = parseMarkdownIntoAttributedString(self.presentationData.strings.MediaPlayer_SavedMusic_RemoveFromProfile, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
@ -1125,7 +1124,6 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
)) ))
profileAudioOffset = 18.0 profileAudioOffset = 18.0
} else { } else {
//TODO:localize
profileAudioComponent = AnyComponent(ButtonComponent( profileAudioComponent = AnyComponent(ButtonComponent(
background: ButtonComponent.Background( background: ButtonComponent.Background(
color: self.presentationData.theme.list.itemCheckColors.fillColor, color: self.presentationData.theme.list.itemCheckColors.fillColor,
@ -1139,7 +1137,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
BundleIconComponent(name: "Peer Info/SaveMusic", tintColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) BundleIconComponent(name: "Peer Info/SaveMusic", tintColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
)), )),
AnyComponentWithIdentity(id: "label", component: AnyComponent( AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Add to Profile", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: self.presentationData.strings.MediaPlayer_SavedMusic_AddToProfile, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)))
)) ))
], spacing: 8.0) ], spacing: 8.0)
)), )),

View File

@ -3768,8 +3768,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return GiftViewScreen(context: context, subject: .message(message), shareStory: shareStory) return GiftViewScreen(context: context, subject: .message(message), shareStory: shareStory)
} }
public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController { public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController {
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory) let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory, openChatTheme: openChatTheme)
controller.disposed = { controller.disposed = {
dismissed?() dismissed?()
} }

View File

@ -8,6 +8,7 @@ import TelegramCore
import AccountContext import AccountContext
import SwiftSignalKit import SwiftSignalKit
import WallpaperResources import WallpaperResources
import StickerResources
import FastBlur import FastBlur
import Svg import Svg
import GZip import GZip
@ -85,6 +86,7 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
var rotation: CGFloat { get set } var rotation: CGFloat { get set }
func update(wallpaper: TelegramWallpaper, animated: Bool) func update(wallpaper: TelegramWallpaper, animated: Bool)
func update(wallpaper: TelegramWallpaper, starGift: StarGift?, animated: Bool)
func _internalUpdateIsSettingUpWallpaper() func _internalUpdateIsSettingUpWallpaper()
func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition)
func updateIsLooping(_ isLooping: Bool) func updateIsLooping(_ isLooping: Bool)
@ -758,11 +760,20 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private var validLayout: (CGSize, WallpaperDisplayMode)? private var validLayout: (CGSize, WallpaperDisplayMode)?
private var wallpaper: TelegramWallpaper? private var wallpaper: TelegramWallpaper?
private var starGift: StarGift?
private var modelRectIndex: Int32?
private var modelStickerNode: DefaultAnimatedStickerNodeImpl?
private var isSettingUpWallpaper: Bool = false private var isSettingUpWallpaper: Bool = false
private struct CachedValidPatternImage { private struct CachedValidPatternImage {
let generate: (TransformImageArguments) -> DrawingContext? let generate: (TransformImageArguments) -> DrawingContext?
let generated: ValidPatternGeneratedImage let generated: ValidPatternGeneratedImage
let rects: [WallpaperGiftPatternRect]
let starGift: StarGift?
let symbolImage: UIImage?
let modelRectIndex: Int32?
let image: UIImage let image: UIImage
} }
@ -771,6 +782,10 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private struct ValidPatternImage { private struct ValidPatternImage {
let wallpaper: TelegramWallpaper let wallpaper: TelegramWallpaper
let invertPattern: Bool let invertPattern: Bool
let rects: [WallpaperGiftPatternRect]
let starGift: StarGift?
let symbolImage: UIImage?
let modelRectIndex: Int32?
let generate: (TransformImageArguments) -> DrawingContext? let generate: (TransformImageArguments) -> DrawingContext?
} }
private var validPatternImage: ValidPatternImage? private var validPatternImage: ValidPatternImage?
@ -781,10 +796,38 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
let patternColor: UInt32 let patternColor: UInt32
let backgroundColor: UInt32 let backgroundColor: UInt32
let invertPattern: Bool let invertPattern: Bool
let starGift: StarGift?
let modelRectIndex: Int32?
public static func ==(lhs: ValidPatternGeneratedImage, rhs: ValidPatternGeneratedImage) -> Bool {
if lhs.wallpaper != rhs.wallpaper {
return false
}
if lhs.size != rhs.size {
return false
}
if lhs.patternColor != rhs.patternColor {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.invertPattern != rhs.invertPattern {
return false
}
if lhs.starGift?.slug != rhs.starGift?.slug {
return false
}
if lhs.modelRectIndex != rhs.modelRectIndex {
return false
}
return true
}
} }
private var validPatternGeneratedImage: ValidPatternGeneratedImage? private var validPatternGeneratedImage: ValidPatternGeneratedImage?
private let patternImageDisposable = MetaDisposable() private let patternImageDisposable = MetaDisposable()
private let symbolImageDisposable = MetaDisposable()
private var bubbleTheme: PresentationTheme? private var bubbleTheme: PresentationTheme?
private var bubbleCorners: PresentationChatBubbleCorners? private var bubbleCorners: PresentationChatBubbleCorners?
@ -930,11 +973,26 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
} }
public func update(wallpaper: TelegramWallpaper, animated: Bool) { public func update(wallpaper: TelegramWallpaper, animated: Bool) {
if self.wallpaper == wallpaper { self.update(wallpaper: wallpaper, starGift: nil, animated: animated)
}
public func update(wallpaper: TelegramWallpaper, starGift: StarGift?, animated: Bool) {
if self.wallpaper == wallpaper && self.starGift == starGift {
return return
} }
let previousWallpaper = self.wallpaper let previousWallpaper = self.wallpaper
let previousStarGift = self.starGift
self.wallpaper = wallpaper self.wallpaper = wallpaper
self.starGift = starGift
if previousWallpaper != wallpaper || previousStarGift?.slug != starGift?.slug {
if let _ = starGift {
self.modelRectIndex = Int32.random(in: 0 ..< 10)
} else {
self.modelRectIndex = nil
}
}
if let _ = previousWallpaper, animated { if let _ = previousWallpaper, animated {
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
@ -1132,6 +1190,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
} }
default: default:
self.patternImageDisposable.set(nil) self.patternImageDisposable.set(nil)
self.symbolImageDisposable.set(nil)
self.validPatternImage = nil self.validPatternImage = nil
self.patternImageLayer.isHidden = true self.patternImageLayer.isHidden = true
self.patternImageLayer.fillWithColorUntilLoaded = nil self.patternImageLayer.fillWithColorUntilLoaded = nil
@ -1147,6 +1206,9 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
return return
} }
let starGift = self.starGift
let modelRectIndex = self.modelRectIndex
var invertPattern: Bool = false var invertPattern: Bool = false
var patternIsLight: Bool = false var patternIsLight: Bool = false
@ -1170,12 +1232,19 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
} }
} }
if let previousStarGift = self.validPatternImage?.starGift, !updated {
updated = true
if previousStarGift.slug == starGift?.slug {
updated = false
}
}
if updated { if updated {
self.validPatternGeneratedImage = nil self.validPatternGeneratedImage = nil
self.validPatternImage = nil self.validPatternImage = nil
if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper && cachedValidPatternImage.generated.invertPattern == invertPattern { if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper && cachedValidPatternImage.generated.invertPattern == invertPattern && cachedValidPatternImage.starGift == starGift && cachedValidPatternImage.modelRectIndex == modelRectIndex {
self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, invertPattern: invertPattern, generate: cachedValidPatternImage.generate) self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, invertPattern: invertPattern, rects: cachedValidPatternImage.rects, starGift: cachedValidPatternImage.starGift, symbolImage: cachedValidPatternImage.symbolImage, modelRectIndex: cachedValidPatternImage.modelRectIndex, generate: cachedValidPatternImage.generate)
} else { } else {
func reference(for resource: EngineMediaResource, media: EngineMedia) -> MediaResourceReference { func reference(for resource: EngineMediaResource, media: EngineMedia) -> MediaResourceReference {
return .wallpaper(wallpaper: .slug(file.slug), resource: resource._asResource()) return .wallpaper(wallpaper: .slug(file.slug), resource: resource._asResource())
@ -1189,37 +1258,33 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: reference(for: EngineMediaResource(file.file.resource), media: EngineMedia(file.file)))) convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: reference(for: EngineMediaResource(file.file.resource), media: EngineMedia(file.file))))
let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true)
self.patternImageDisposable.set((signal var symbolImage: Signal<UIImage?, NoError> = .single(nil)
|> deliverOnMainQueue).start(next: { [weak self] generator in if let starGift = self.starGift, case let .unique(uniqueGift) = starGift {
guard let strongSelf = self else { for attribute in uniqueGift.attributes {
if case let .pattern(_, file, _) = attribute, let dimensions = file.dimensions {
let size = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
symbolImage = chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: size)
|> map { generator -> UIImage? in
return generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero))?.generateImage()
}
break
}
}
}
self.patternImageDisposable.set(combineLatest(queue: Queue.mainQueue(), signal, symbolImage).start(next: { [weak self] generator, symbolImage in
guard let self else {
return return
} }
if let (generator, rects) = generator {
if let generator = generator { self.validPatternImage = ValidPatternImage(wallpaper: wallpaper, invertPattern: invertPattern, rects: rects, starGift: starGift, symbolImage: symbolImage, modelRectIndex: modelRectIndex, generate: generator)
/*generator = { arguments in self.validPatternGeneratedImage = nil
let scale = arguments.scale ?? UIScreenScale if let (size, displayMode) = self.validLayout {
let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) self.loadPatternForSizeIfNeeded(size: size, displayMode: displayMode, transition: .immediate)
context.withFlippedContext { c in
if let path = getAppBundle().path(forResource: "PATTERN_static", ofType: "svg"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
if let image = drawSvgImage(data, CGSize(width: arguments.drawingSize.width * scale, height: arguments.drawingSize.height * scale), .clear, .black, false) {
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize))
}
}
}
return context
}*/
strongSelf.validPatternImage = ValidPatternImage(wallpaper: wallpaper, invertPattern: invertPattern, generate: generator)
strongSelf.validPatternGeneratedImage = nil
if let (size, displayMode) = strongSelf.validLayout {
strongSelf.loadPatternForSizeIfNeeded(size: size, displayMode: displayMode, transition: .immediate)
} else { } else {
strongSelf._isReady.set(true) self._isReady.set(true)
} }
} else { } else {
strongSelf._isReady.set(true) self._isReady.set(true)
} }
})) }))
} }
@ -1244,7 +1309,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.patternImageLayer.backgroundColor = nil self.patternImageLayer.backgroundColor = nil
} }
let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern) let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern, starGift: starGift, modelRectIndex: modelRectIndex)
if self.validPatternGeneratedImage != updatedGeneratedImage { if self.validPatternGeneratedImage != updatedGeneratedImage {
self.validPatternGeneratedImage = updatedGeneratedImage self.validPatternGeneratedImage = updatedGeneratedImage
@ -1256,7 +1321,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.patternImageLayer.suspendCompositionUpdates = false self.patternImageLayer.suspendCompositionUpdates = false
self.patternImageLayer.updateCompositionIfNeeded() self.patternImageLayer.updateCompositionIfNeeded()
} else { } else {
let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false, displayMode: displayMode.argumentsDisplayMode), scale: min(2.0, UIScreenScale)) let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false, displayMode: displayMode.argumentsDisplayMode, symbolImage: generateTintedImage(image: validPatternImage.symbolImage, color: .white), modelRectIndex: self.modelRectIndex), scale: min(2.0, UIScreenScale))
if self.useSharedAnimationPhase || self.patternImageLayer.contents == nil { if self.useSharedAnimationPhase || self.patternImageLayer.contents == nil {
if let drawingContext = validPatternImage.generate(patternArguments) { if let drawingContext = validPatternImage.generate(patternArguments) {
if let image = drawingContext.generateImage() { if let image = drawingContext.generateImage() {
@ -1267,7 +1332,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.patternImageLayer.updateCompositionIfNeeded() self.patternImageLayer.updateCompositionIfNeeded()
if self.useSharedAnimationPhase { if self.useSharedAnimationPhase {
WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, rects: validPatternImage.rects, starGift: validPatternImage.starGift, symbolImage: validPatternImage.symbolImage, modelRectIndex: validPatternImage.modelRectIndex, image: image)
} }
} else { } else {
self.updatePatternPresentation() self.updatePatternPresentation()
@ -1288,7 +1353,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
strongSelf.updatePatternPresentation() strongSelf.updatePatternPresentation()
if let image = image, strongSelf.useSharedAnimationPhase { if let image = image, strongSelf.useSharedAnimationPhase {
WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, rects: validPatternImage.rects, starGift: validPatternImage.starGift, symbolImage: validPatternImage.symbolImage, modelRectIndex: validPatternImage.modelRectIndex, image: image)
} }
} }
} }
@ -1307,6 +1372,68 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
} }
} }
var modelFile: TelegramMediaFile?
if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, let starGift = validPatternImage.starGift {
if case let .unique(uniqueGift) = starGift {
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
modelFile = file
}
}
}
}
if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, var modelRectIndex = self.modelRectIndex, let modelFile {
let filteredRects = validPatternImage.rects.filter { $0.center.y > $0.containerSize.height * 0.1 && $0.center.y < $0.containerSize.height * 0.9 }
modelRectIndex = modelRectIndex % Int32(filteredRects.count);
let rect = filteredRects[Int(modelRectIndex)]
let modelStickerNode: DefaultAnimatedStickerNodeImpl
if let current = self.modelStickerNode {
modelStickerNode = current
} else {
modelStickerNode = DefaultAnimatedStickerNodeImpl()
modelStickerNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: modelFile.resource, isVideo: false), width: 96, height: 96, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
modelStickerNode.visibility = true
self.modelStickerNode = modelStickerNode
self.addSubnode(modelStickerNode)
}
let targetSize: CGSize = self.bounds.size
let containerSize: CGSize = rect.containerSize
let useAspectFit: Bool = false
let renderScale: CGFloat = useAspectFit
? min(targetSize.width / containerSize.width, targetSize.height / containerSize.height)
: max(targetSize.width / containerSize.width, targetSize.height / containerSize.height)
let drawingSize = CGSize(width: containerSize.width * renderScale, height: containerSize.height * renderScale)
let offsetX = (targetSize.width - drawingSize.width) * 0.5
let offsetY = (targetSize.height - drawingSize.height) * 0.5
let onScreenCenter = CGPoint(x: offsetX + rect.center.x * renderScale, y: offsetY + rect.center.y * renderScale)
let side = rect.side * rect.scale * renderScale
modelStickerNode.bounds = CGRect(origin: .zero, size: CGSize(width: side, height: side))
modelStickerNode.position = onScreenCenter
modelStickerNode.updateLayout(size: modelStickerNode.bounds.size)
modelStickerNode.alpha = 0.5
modelStickerNode.layer.transform = CATransform3DMakeRotation(rect.rotation, 0, 0, 1)
} else {
if let modelStickerNode = self.modelStickerNode {
self.modelStickerNode = nil
if transition.isAnimated {
modelStickerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak modelStickerNode] _ in
modelStickerNode?.removeFromSupernode()
})
} else {
modelStickerNode.removeFromSupernode()
}
}
}
transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size))
} }
@ -1559,3 +1686,14 @@ private protocol WallpaperComponentView: AnyObject {
public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false) -> WallpaperBackgroundNode { public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false) -> WallpaperBackgroundNode {
return WallpaperBackgroundNodeImpl(context: context, useSharedAnimationPhase: useSharedAnimationPhase) return WallpaperBackgroundNodeImpl(context: context, useSharedAnimationPhase: useSharedAnimationPhase)
} }
private extension StarGift {
var slug: String? {
switch self {
case let .unique(uniqueGift):
return uniqueGift.slug
default:
return nil
}
}
}

View File

@ -353,14 +353,18 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments {
let customPatternColor: UIColor? let customPatternColor: UIColor?
let bakePatternAlpha: CGFloat let bakePatternAlpha: CGFloat
let displayMode: DisplayMode let displayMode: DisplayMode
let symbolImage: UIImage?
let modelRectIndex: Int32?
public init(colors: [UIColor], rotation: Int32?, customPatternColor: UIColor? = nil, preview: Bool = false, bakePatternAlpha: CGFloat = 1.0, displayMode: DisplayMode = .aspectFill) { public init(colors: [UIColor], rotation: Int32?, customPatternColor: UIColor? = nil, preview: Bool = false, bakePatternAlpha: CGFloat = 1.0, displayMode: DisplayMode = .aspectFill, symbolImage: UIImage? = nil, modelRectIndex: Int32? = nil) {
self.colors = colors self.colors = colors
self.rotation = rotation self.rotation = rotation
self.customPatternColor = customPatternColor self.customPatternColor = customPatternColor
self.preview = preview self.preview = preview
self.bakePatternAlpha = bakePatternAlpha self.bakePatternAlpha = bakePatternAlpha
self.displayMode = displayMode self.displayMode = displayMode
self.symbolImage = symbolImage
self.modelRectIndex = modelRectIndex
} }
public func serialized() -> NSArray { public func serialized() -> NSArray {
@ -373,6 +377,9 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments {
array.add(NSNumber(value: self.preview)) array.add(NSNumber(value: self.preview))
array.add(NSNumber(value: Double(self.bakePatternAlpha))) array.add(NSNumber(value: Double(self.bakePatternAlpha)))
array.add(NSNumber(value: self.displayMode.rawValue)) array.add(NSNumber(value: self.displayMode.rawValue))
if let symbolImage {
array.add(symbolImage)
}
return array return array
} }
} }
@ -470,18 +477,34 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana
} }
} }
public func patternWallpaperImage(account: Account, accountManager: AccountManager<TelegramAccountManagerTypes>, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { public struct WallpaperGiftPatternRect: Equatable {
public let containerSize: CGSize
public let center: CGPoint
public let side: CGFloat
public let scale: CGFloat
public let rotation: CGFloat
fileprivate init(containerSize: CGSize, rect: GiftPatternRect) {
self.containerSize = containerSize
self.center = rect.center
self.side = rect.side
self.scale = rect.scale
self.rotation = rect.rotation
}
}
public func patternWallpaperImage(account: Account, accountManager: AccountManager<TelegramAccountManagerTypes>, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false, forcePrepared: Bool = false) -> Signal<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, NoError> {
return patternWallpaperDatas(account: account, accountManager: accountManager, representations: representations, mode: mode, autoFetchFullSize: autoFetchFullSize) return patternWallpaperDatas(account: account, accountManager: accountManager, representations: representations, mode: mode, autoFetchFullSize: autoFetchFullSize)
|> mapToSignal { fullSizeData, fullSizeComplete in |> mapToSignal { fullSizeData, fullSizeComplete in
if !autoFetchFullSize || fullSizeComplete { if !autoFetchFullSize || fullSizeComplete {
return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode) return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode, forcePrepared: forcePrepared)
} else { } else {
return .single(nil) return .single(nil)
} }
} }
} }
private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode, forcePrepared: Bool = false) -> Signal<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, NoError> {
var prominent = false var prominent = false
if case .thumbnail = mode { if case .thumbnail = mode {
prominent = true prominent = true
@ -491,7 +514,11 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
return .single((fullSizeData, fullSizeComplete)) return .single((fullSizeData, fullSizeComplete))
|> map { fullSizeData, fullSizeComplete in |> map { fullSizeData, fullSizeComplete in
return { arguments in var rects: [WallpaperGiftPatternRect] = []
if let fullSizeData, let patternData = getGiftPatternData(fullSizeData) {
rects = patternData.rects.map { WallpaperGiftPatternRect(containerSize: patternData.size, rect: $0) }
}
return ({ arguments in
var scale = scale var scale = scale
if scale.isZero { if scale.isZero {
scale = arguments.scale ?? UIScreenScale scale = arguments.scale ?? UIScreenScale
@ -561,7 +588,7 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
var image: UIImage? var image: UIImage?
if let fullSizeData = fullSizeData { if let fullSizeData = fullSizeData {
if mode == .screen { if mode == .screen {
image = renderPreparedImage(fullSizeData, CGSize(width: size.width * context.scale, height: size.height * context.scale), .black, 1.0, displayMode == .aspectFit) image = renderPreparedImageWithSymbol(fullSizeData, CGSize(width: size.width * context.scale, height: size.height * context.scale), .black, 1.0, displayMode == .aspectFit, customArguments.symbolImage, customArguments.modelRectIndex ?? -1)
} else { } else {
image = UIImage(data: fullSizeData) image = UIImage(data: fullSizeData)
} }
@ -674,7 +701,7 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
} else { } else {
return nil return nil
} }
} }, rects)
} }
} }
@ -1452,6 +1479,10 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
wallpaperSignal = .single((backgroundColor, incomingColors, outgoingColors, image, options.blur, false, 1.0, rotation)) wallpaperSignal = .single((backgroundColor, incomingColors, outgoingColors, image, options.blur, false, 1.0, rotation))
} }
case let .file(file): case let .file(file):
if file.settings.intensity == 100 {
print()
}
rotation = file.settings.rotation rotation = file.settings.rotation
if file.isPattern, let intensity = file.settings.intensity, intensity < 0 { if file.isPattern, let intensity = file.settings.intensity, intensity < 0 {
backgroundColor = (.black, nil, []) backgroundColor = (.black, nil, [])
@ -1464,6 +1495,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
} else { } else {
backgroundColor = (theme.chatList.backgroundColor, nil, []) backgroundColor = (theme.chatList.backgroundColor, nil, [])
} }
wallpaperSignal = cachedWallpaper(account: account, slug: file.slug, settings: file.settings) wallpaperSignal = cachedWallpaper(account: account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper in |> mapToSignal { wallpaper in
if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper {
@ -1479,6 +1511,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
let convertedPreviewRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map { let convertedPreviewRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map {
ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource)) ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource))
} }
let useFallback = convertedPreviewRepresentations.isEmpty
var convertedRepresentations: [ImageRepresentationWithReference] = [] var convertedRepresentations: [ImageRepresentationWithReference] = []
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
@ -1511,12 +1544,11 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
let isLight = UIColor.average(of: file.settings.colors.map(UIColor.init(rgb:))).hsb.b > 0.3 let isLight = UIColor.average(of: file.settings.colors.map(UIColor.init(rgb:))).hsb.b > 0.3
arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white)
} }
return patternWallpaperImage(account: account, accountManager: accountManager, representations: useFallback ? convertedRepresentations : convertedPreviewRepresentations, mode: useFallback ? .screen : .thumbnail, autoFetchFullSize: true)
return patternWallpaperImage(account: account, accountManager: accountManager, representations: convertedPreviewRepresentations, mode: .thumbnail, autoFetchFullSize: true) |> mapToSignal { generatorAndRects -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in
|> mapToSignal { generator -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in
let imageSize = CGSize(width: 148.0, height: 320.0) let imageSize = CGSize(width: 148.0, height: 320.0)
let imageArguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, custom: arguments) let imageArguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, custom: arguments)
let context = generator?(imageArguments) let context = generatorAndRects?.generator(imageArguments)
let image = context?.generateImage() let image = context?.generateImage()
if !file.settings.colors.isEmpty { if !file.settings.colors.isEmpty {
@ -1771,7 +1803,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
c.restoreGState() c.restoreGState()
} else { } else {
let rect = CGRect(x: 8.0, y: arguments.drawingSize.height - 24.0 - 9.0 - 3.0, width: arguments.drawingSize.width - 8.0 * 2.0, height: 24.0) let rect = CGRect(x: 8.0, y: arguments.drawingSize.height - 24.0 - 9.0 - 3.0, width: 48.0, height: 24.0)
c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath)
c.clip() c.clip()

View File

@ -1,5 +1,5 @@
{ {
"app": "11.15", "app": "12.0",
"xcode": "16.2", "xcode": "16.2",
"bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217", "bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217",
"macos": "15" "macos": "15"