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.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 makeStarsIntroScreen(context: AccountContext) -> 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 makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController

View File

@ -2503,7 +2503,7 @@ public final class ChatListNode: ListView {
case let .user(userType):
if case let .user(user) = peer {
match = true
if user.id.isVerificationCodes {
if user.id.isVerificationCodes || user.id.isTelegramNotifications {
match = false
}
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 isScrollEnabled: Bool
public let hasDimView: Bool
public let autoAnimateOut: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void
@ -77,6 +78,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
clipsContent: Bool = false,
isScrollEnabled: Bool = true,
hasDimView: Bool = true,
autoAnimateOut: Bool = true,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {},
@ -88,6 +90,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.clipsContent = clipsContent
self.isScrollEnabled = isScrollEnabled
self.hasDimView = hasDimView
self.autoAnimateOut = autoAnimateOut
self.externalState = externalState
self.animateOut = animateOut
self.onPan = onPan
@ -110,6 +113,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if lhs.hasDimView != rhs.hasDimView {
return false
}
if lhs.autoAnimateOut != rhs.autoAnimateOut {
return false
}
if lhs.animateOut != rhs.animateOut {
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) {
self.animateIn()
} 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
return availableSize

View File

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

View File

@ -4,8 +4,28 @@
#import <Foundation/Foundation.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);
GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data);
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);

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[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($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[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($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[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($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[400029819] = { return Api.account.ConnectedBots.parse_connectedBots($0) }
dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) }

View File

@ -289,7 +289,7 @@ public extension Api {
public extension Api {
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 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) {
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 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)}
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 {
buffer.appendInt32(648369470)
buffer.appendInt32(468707429)
}
serializeInt32(flags, 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 << 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 << 10) != 0 {themePeer!.serialize(buffer, true)}
break
}
}
@ -352,8 +353,8 @@ public extension Api {
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):
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):
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)])
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), ("themePeer", themePeer as Any)])
}
}
@ -463,6 +464,10 @@ public extension Api {
if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() }
var _17: String?
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 _c2 = _2 != nil
let _c3 = _3 != nil
@ -480,8 +485,9 @@ public extension Api {
let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != 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 {
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)
let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil
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 {
return nil

View File

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

View File

@ -359,7 +359,9 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
resellForTonOnly: false,
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil
valueCurrency: nil,
flags: [],
themePeerId: nil
)
if let entry = CodableEntry(RecentStarGiftItem(gift)) {
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 func currentLayer() -> UInt {
return 213
return 214
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -230,6 +230,37 @@ public enum TelegramWallpaper: Equatable {
self.file = file
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)

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 typealias Result = CachedUserData.BotPreview?

View File

@ -318,6 +318,18 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case releasedBy
case valueAmount
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 {
@ -593,8 +605,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let releasedBy: EnginePeer.Id?
public let valueAmount: Int64?
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.giftId = giftId
self.title = title
@ -609,6 +623,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = releasedBy
self.valueAmount = valueAmount
self.valueCurrency = valueCurrency
self.flags = flags
self.themePeerId = themePeerId
}
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.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount)
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) {
@ -672,6 +690,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) }
self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.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 {
@ -697,6 +717,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy)
try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount)
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) {
@ -738,6 +760,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
encoder.encodeNil(forKey: CodingKeys.valueAmount.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 {
@ -755,7 +783,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy,
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,
releasedBy: self.releasedBy,
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
}
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
if let ownerAddress {
owner = .address(ownerAddress)
@ -896,7 +949,11 @@ extension StarGift {
return nil
}
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 filter: ProfileGiftsContext.Filters
private var limit: Int32
private var gifts: [ProfileGiftsContext.State.StarGift] = []
private var count: Int32?
@ -1345,7 +1403,8 @@ private final class ProfileGiftsContextImpl {
peerId: EnginePeer.Id,
collectionId: Int32?,
sorting: ProfileGiftsContext.Sorting,
filter: ProfileGiftsContext.Filters
filter: ProfileGiftsContext.Filters,
limit: Int32
) {
self.queue = queue
self.account = account
@ -1353,6 +1412,7 @@ private final class ProfileGiftsContextImpl {
self.collectionId = collectionId
self.sorting = sorting
self.filter = filter
self.limit = limit
self.loadMore()
}
@ -1377,6 +1437,7 @@ private final class ProfileGiftsContextImpl {
let postbox = self.account.postbox
let filter = self.filter
let sorting = self.sorting
let limit = self.limit
let isFiltered = self.filter != .All || self.sorting != .date
if !isFiltered {
@ -1464,7 +1525,7 @@ private final class ProfileGiftsContextImpl {
if !filter.contains(.unique) {
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)
|> `catch` { _ -> Signal<Api.payments.SavedStarGifts?, NoError> in
return .single(nil)
@ -2363,14 +2424,15 @@ public final class ProfileGiftsContext {
peerId: EnginePeer.Id,
collectionId: Int32? = nil,
sorting: ProfileGiftsContext.Sorting = .date,
filter: ProfileGiftsContext.Filters = .All
filter: ProfileGiftsContext.Filters = .All,
limit: Int32 = 36
) {
self.peerId = peerId
self.collectionId = collectionId
let queue = self.queue
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)
} else if error.errorDescription == "STARGIFT_USAGE_LIMITED" {
return .fail(.starGiftOutOfStock)
} else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" {
return .fail(.starGiftUserLimit)
}
return .fail(.generic)
}

View File

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

View File

@ -47,13 +47,36 @@ public func makePresentationTheme(cloudTheme: TelegramTheme, dark: Bool = false)
} else {
settings = nil
}
guard let settings = settings else {
guard let settings else {
return nil
}
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)
}
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? {
let settings: TelegramThemeSettings?
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 preview: Bool
public var forceSync: Bool = false
public var starGift: StarGift?
public let resourceCache: PresentationsResourceCache = PresentationsResourceCache()

View File

@ -781,12 +781,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
var emoji = ""
var additionalAttributes: [String: Any] = [:]
var giftTitle: String?
switch chatTheme {
case let .emoticon(emoticon):
emoji = emoticon
case let .gift(starGift, _):
var file: TelegramMediaFile?
if case let .unique(uniqueGift) = starGift {
giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: dateTimeFormat))"
for attribute in uniqueGift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
@ -802,11 +805,21 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
attributedString = NSAttributedString(string: strings.Notification_ChannelChangedTheme(emoji).string, font: titleFont, textColor: primaryTextColor)
} else if message.author?.id == accountPeerId {
let resultTitleString = strings.Notification_YouChangedTheme(emoji)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: emojiAttributes])
if let giftTitle {
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 {
let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: emojiAttributes])
if let giftTitle {
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):
@ -817,8 +830,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
let price: String
if currency == "XTR" {
//TODO:localize
price = "\(amount) Stars"
price = strings.Notification_PremiumGift_Stars(Int32(clamping: amount))
} else {
price = formatCurrencyAmount(amount, currency: currency)
}
@ -833,7 +845,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case let .giftStars(currency, amount, count, _, _, _):
let _ = count
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 {
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
@ -1155,17 +1168,22 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = mutableString
case .prizeStars:
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 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()))
attributedString = mutableAttributedString
} 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 {
var finalPrice = gift.price
if let upgradeStars {
if let upgradeStars, !upgradeSeparate {
finalPrice += upgradeStars
}
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/FaceScanScreen",
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ContactsHelper",
] + select({
"@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 {
result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
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 {
if !canAddMessageReactions(message: message) {
needReactions = false

View File

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

View File

@ -24,6 +24,7 @@ import ChatMessageItemCommon
import TextNodeWithEntities
import InvisibleInkDustNode
import PeerInfoCoverComponent
import GiftItemComponent
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
@ -45,6 +46,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
private let giftIcon = ComponentView<Empty>()
private let modelTitleTextNode: TextNode
private let modelValueTextNode: TextNode
@ -416,6 +418,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var months: Int32 = 3
var animationName: String = ""
var animationFile: TelegramMediaFile?
var uniqueGift: StarGift.UniqueGift?
var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = ""
var subtitleColor = primaryTextColor
@ -708,6 +711,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded
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:
break
}
@ -866,6 +883,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
giftSize.height += 12.0
}
if let _ = uniqueGift {
giftSize.height -= 31.0
}
var labelRects = labelLayout.linesRects()
if labelRects.count > 1 {
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.creatorButtonTitleNode.isHidden = creatorButtonTitle.isEmpty
if strongSelf.item == nil && !isStoryEntity {
if strongSelf.item == nil && !isStoryEntity && uniqueGift == nil {
strongSelf.animationNode.started = { [weak self] in
if let strongSelf = self {
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)
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
@ -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)
if let (offset, image) = backgroundMaskImage {
if strongSelf.backgroundNode == nil {
@ -1524,7 +1573,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if isPlaying {
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 unreadRange.contains(item.message.id.id) {
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)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value = value {
return .single(value)
return .single(value.generator)
} else {
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)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value {
return .single(value)
return .single(value.generator)
} else {
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 ShimmerEffect
import AttachmentUI
import AvatarNode
private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
let index: Int
let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool
var selected: Bool
let theme: PresentationTheme
let strings: PresentationStrings
let wallpaper: TelegramWallpaper?
var stableId: Int {
return index
var stableId: String {
return self.chatTheme?.id ?? "\(self.index)"
}
static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool {
@ -47,6 +49,9 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
if lhs.themeReference?.index != rhs.themeReference?.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.nightMode != rhs.nightMode {
return false
}
@ -70,16 +75,16 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
}
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 {
let context: AccountContext
let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool
let selected: Bool
let theme: PresentationTheme
@ -87,11 +92,24 @@ private class ThemeSettingsThemeIconItem: ListViewItem {
let wallpaper: TelegramWallpaper?
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.chatTheme = chatTheme
self.emojiFile = emojiFile
self.themeReference = themeReference
self.peer = peer
self.nightMode = nightMode
self.selected = selected
self.theme = theme
@ -240,6 +258,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
private let emojiImageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode
private var bubbleNode: ASImageNode?
private var avatarNode: AvatarNode?
private var replaceNode: ASImageNode?
var snapshotView: UIView?
var item: ThemeSettingsThemeIconItem?
@ -489,6 +510,75 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
animatedStickerNode.frame = emojiFrame
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 {
static let themeCrossfadeDuration: Double = 0.3
static let themeCrossfadeDelay: Double = 0.25
public final class ChatThemeScreen: ViewController {
public static let themeCrossfadeDuration: Double = 0.3
public static let themeCrossfadeDelay: Double = 0.25
private var controllerNode: ChatThemeScreenNode {
return self.displayNode as! ChatThemeScreenNode
@ -539,7 +629,7 @@ final class ChatThemeScreen: ViewController {
private let animatedEmojiStickers: [String: [StickerPackItem]]
private let initiallySelectedTheme: ChatTheme?
private let peerName: String
let canResetWallpaper: Bool
fileprivate let canResetWallpaper: Bool
private let previewTheme: (ChatTheme?, Bool?) -> Void
fileprivate let changeWallpaper: () -> Void
fileprivate let resetWallpaper: () -> Void
@ -548,9 +638,9 @@ final class ChatThemeScreen: ViewController {
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
var dismissed: (() -> Void)?
public var dismissed: (() -> Void)?
var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
public var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl
@ -558,7 +648,7 @@ final class ChatThemeScreen: ViewController {
}
}
init(
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>),
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
if let controller = controller as? TooltipScreen {
controller.dismiss()
@ -683,7 +773,7 @@ final class ChatThemeScreen: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
func dimTapped() {
public func dimTapped() {
self.controllerNode.dimTapped()
}
}
@ -743,6 +833,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
private var initialized = false
private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext
private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State?
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.doneButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.doneButton.isUserInteractionEnabled = false
if strongSelf.doneButton.font == .bold {
strongSelf.completion?(strongSelf.selectedTheme)
strongSelf.complete()
} else {
strongSelf.controller?.changeWallpaper()
}
@ -888,13 +978,37 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
self.disposable.set(combineLatest(
queue: Queue.mainQueue(),
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.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 {
return
}
let (uniqueGiftChatThemesState, peers) = uniqueGiftChatThemesStateAndPeers
strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState
let isFirstTime = strongSelf.entries == nil
let presentationData = strongSelf.presentationData
@ -905,61 +1019,91 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
chatTheme: nil,
emojiFile: nil,
themeReference: nil,
peer: nil,
nightMode: false,
selected: selectedTheme == nil,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
for theme in themes {
guard let emoticon = theme.emoticon else {
continue
var giftThemes = uniqueGiftChatThemesState.themes
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
}
var emojiFile: TelegramMediaFile?
var peer: EnginePeer?
if case let .unique(uniqueGift) = gift {
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
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(
index: entries.count,
chatTheme: theme,
emojiFile: emojiFile,
themeReference: nil,
themeReference: themeReference,
peer: peer,
nightMode: isDarkAppearance,
selected: selectedTheme?.id == theme.id,
theme: presentationData.theme,
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
if let self, self.selectedTheme != chatTheme {
self.setChatTheme(chatTheme)
}
}
let previousEntries = strongSelf.entries ?? []
let crossfade = previousEntries.count != entries.count
let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade)
//let crossfade = previousEntries.count != entries.count
let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: false)
strongSelf.enqueueTransition(transition)
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()
}
@ -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() {
if self.selectedTheme?.id == self.initiallySelectedTheme?.id {
self.cancelButtonPressed()
} 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
if let strongSelf = self {
strongSelf.completion?(strongSelf.selectedTheme)
if let self {
self.complete()
}
})], actionLayout: .horizontal, dismissOnOutsideTap: true)
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:
if let perUserLimit, let giftFile {
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)
return
}

View File

@ -277,7 +277,7 @@ final class GiftStoreScreenComponent: Component {
buyGift: { slug, peerId, price in
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()
}
)

View File

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

View File

@ -149,10 +149,6 @@ private final class GiftValueSheetContent: CombinedComponent {
gift: gift
)
controller.push(storeController)
Queue.mainQueue().after(2.0, {
controller.dismiss(animated: false)
})
}
func openGiftFragmentResale(url: String) {
@ -205,8 +201,6 @@ private final class GiftValueSheetContent: CombinedComponent {
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
//let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
//let controller = environment.controller
let state = context.state
@ -319,12 +313,12 @@ private final class GiftValueSheetContent: CombinedComponent {
var descriptionText: String
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 {
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 {
descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Telegram."
descriptionText = strings.Gift_Value_DescriptionLastPriceTelegram(giftTitle).string
}
}
if !descriptionText.isEmpty {
@ -394,7 +388,7 @@ private final class GiftValueSheetContent: CombinedComponent {
tableItems.append(.init(
id: "initialDate",
title: "Initial Sale",
title: strings.Gift_Value_InitialSale,
component: AnyComponent(
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(
id: "initialPrice",
title: "Initial Price",
title: strings.Gift_Value_InitialPrice,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
@ -425,7 +419,7 @@ private final class GiftValueSheetContent: CombinedComponent {
if let lastSaleDate = component.valueInfo.lastSaleDate {
tableItems.append(.init(
id: "lastDate",
title: "Last Sale",
title: strings.Gift_Value_LastSale,
component: AnyComponent(
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
)),
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))
@ -467,7 +461,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
tableItems.append(.init(
id: "lastPrice",
title: "Last Price",
title: strings.Gift_Value_LastPrice,
hasBackground: false,
component: itemComponent
))
@ -494,8 +488,7 @@ private final class GiftValueSheetContent: CombinedComponent {
color: theme.list.itemAccentColor
)),
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))
))
@ -504,7 +497,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
tableItems.append(.init(
id: "floorPrice",
title: "Minumum Price",
title: strings.Gift_Value_MinimumPrice,
hasBackground: false,
component: itemComponent
))
@ -531,7 +524,7 @@ private final class GiftValueSheetContent: CombinedComponent {
color: theme.list.itemAccentColor
)),
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))
))
@ -540,7 +533,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
tableItems.append(.init(
id: "averagePrice",
title: "Average Price",
title: strings.Gift_Value_AveragePrice,
hasBackground: false,
component: itemComponent
))
@ -587,7 +580,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
)),
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(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
@ -639,7 +632,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
)),
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(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
@ -724,6 +717,7 @@ final class GiftValueSheetComponent: CombinedComponent {
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {

View File

@ -36,6 +36,7 @@ import StarsBalanceOverlayComponent
import BalanceNeededScreen
import GiftItemComponent
import GiftAnimationComponent
import ChatThemeScreen
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -104,6 +105,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var cachedHiddenImage: (UIImage, PresentationTheme)?
var inProgress = false
var canSkip = false
var testUpgradeAnimation = !"".isEmpty
@ -152,7 +154,7 @@ private final class GiftViewSheetContent: CombinedComponent {
super.init()
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
}
@ -595,7 +597,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
if let convertToStars = controller?.convertToStars {
convertToStars()
convertToStars(reference)
} else {
let _ = (self.context.engine.payments.convertStarGift(reference: reference)
|> deliverOnMainQueue).startStandalone()
@ -668,14 +670,34 @@ private final class GiftViewSheetContent: CombinedComponent {
return
}
self.isOpeningValue = true
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug)
|> deliverOnMainQueue).start(next: { [weak self] valueInfo in
guard let self, let valueInfo else {
guard let self else {
return
}
self.isOpeningValue = false
let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo)
controller.push(valueController)
Queue.mainQueue().after(0.2) {
self.isOpeningValue = false
}
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))
}
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() {
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
@ -882,7 +981,7 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let self, let controller else {
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
}, completed: { [weak self, weak controller] in
@ -932,7 +1031,7 @@ private final class GiftViewSheetContent: CombinedComponent {
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
guard let self else {
return
@ -1061,7 +1160,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var items: [ContextMenuItem] = []
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
c?.dismiss(completion: { [weak self, weak controller] in
guard let self, let controller else {
@ -1069,7 +1168,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
let pinnedToTop = !pinnedToTop
if togglePinnedToTop(pinnedToTop) {
if togglePinnedToTop(reference, pinnedToTop) {
if pinnedToTop {
controller.dismissAnimated()
} else {
@ -1113,6 +1212,16 @@ private final class GiftViewSheetContent: CombinedComponent {
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 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() {
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 {
self.inProgress = true
self.updated()
@ -1538,8 +1668,11 @@ private final class GiftViewSheetContent: CombinedComponent {
}
Queue.mainQueue().after(0.5, {
self.inUpgradePreview = false
self.canSkip = true
self.updated(transition: .immediate)
self.inProgress = false
self.inUpgradePreview = false
self.justUpgraded = true
self.revealedNumberDigits = -1
@ -1550,19 +1683,23 @@ private final class GiftViewSheetContent: CombinedComponent {
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.updated(transition: .immediate)
Queue.mainQueue().after(0.7) {
Queue.mainQueue().after(secondDuration) {
self.revealedAttributes.insert(.pattern)
self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) {
Queue.mainQueue().after(thirdDuration) {
self.revealedAttributes.insert(.model)
self.updated(transition: .immediate)
Queue.mainQueue().after(0.55) {
self.canSkip = false
self.updated(transition: .easeInOut(duration: 0.2))
}
Queue.mainQueue().after(0.6) {
if let controller = self.getController() as? GiftViewScreen {
@ -1572,6 +1709,8 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
}
self.updated(transition: .spring(duration: 0.4))
})
return
}
@ -1592,8 +1731,11 @@ private final class GiftViewSheetContent: CombinedComponent {
let context = self.context
let upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
if let upgradeGift = controller.upgradeGift {
guard let reference = arguments.reference else {
return
}
upgradeGiftImpl = { formId, keepOriginalInfo in
return upgradeGift(formId, keepOriginalInfo)
return upgradeGift(formId, reference, keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
@ -1619,6 +1761,9 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
self.canSkip = true
self.updated(transition: .immediate)
self.inProgress = false
self.inUpgradePreview = false
@ -1639,17 +1784,22 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
Queue.mainQueue().after(1.2) {
Queue.mainQueue().after(firstDuration) {
self.revealedAttributes.insert(.backdrop)
self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) {
Queue.mainQueue().after(secondDuration) {
self.revealedAttributes.insert(.pattern)
self.updated(transition: .immediate)
Queue.mainQueue().after(0.7) {
Queue.mainQueue().after(thirdDuration) {
self.revealedAttributes.insert(.model)
self.updated(transition: .immediate)
Queue.mainQueue().after(0.55) {
self.canSkip = false
self.updated(transition: .easeInOut(duration: 0.2))
}
Queue.mainQueue().after(0.6) {
if let controller = self.getController() as? GiftViewScreen {
@ -1661,7 +1811,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
self.subject = .profileGift(peerId, result)
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4))
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),
transition: context.transition
)
headerComponents.append({
context.add(wearPerks
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
})
context.add(wearPerks
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += wearPerks.size.height
originY += 16.0
} else if showUpgradePreview {
@ -3166,8 +3315,8 @@ private final class GiftViewSheetContent: CombinedComponent {
component: PlainButtonComponent(
content: AnyComponent(
HeaderButtonComponent(
title: uniqueGift.resellAmounts == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
iconName: uniqueGift.resellAmounts == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
)
),
effectAlignment: .center,
@ -3629,7 +3778,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} else {
resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })
}
if let resellAmount {
if let resellAmount, wearPeerNameChild == nil {
if incoming || ownerPeerId == component.context.account.peerId {
let priceButton = priceButton.update(
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 textColor = theme.list.itemSecondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
@ -3672,7 +3826,9 @@ private final class GiftViewSheetContent: CombinedComponent {
var addressToOpen: String?
var descriptionText: String
if let uniqueGift, selling {
if isChatTheme {
descriptionText = strings.Gift_View_OpenChatTheme
} else if let uniqueGift, selling {
let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner {
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
@ -3731,7 +3887,10 @@ private final class GiftViewSheetContent: CombinedComponent {
},
tapAction: { [weak state] attributes, _ in
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)
} else {
state?.updateSavedToProfile(!savedToProfile)
@ -3761,7 +3920,25 @@ private final class GiftViewSheetContent: CombinedComponent {
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
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 premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
@ -4081,7 +4258,9 @@ private final class GiftViewSheetContent: CombinedComponent {
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state] in
state?.dismiss(animated: true)
if let state {
state.dismiss(animated: true)
}
}),
availableSize: buttonSize,
transition: context.transition
@ -4090,7 +4269,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size)
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
let upgradeNextButton = upgradeNextButton.update(
@ -4098,7 +4277,7 @@ private final class GiftViewSheetContent: CombinedComponent {
content: AnyComponent(
HStack([
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(
GiftItemComponent(
@ -4278,12 +4457,12 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
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 {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
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
if let peerId, let giftMessageId {
reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId))
@ -4292,7 +4471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
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):
var reference: StarGiftReference
if let peerId, let savedId {
@ -4317,13 +4496,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift {
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:
return nil
}
}
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):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -4333,7 +4512,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
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:
return nil
case .upgradePreview:
@ -4374,13 +4553,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
fileprivate let balanceOverlay = ComponentView<Empty>()
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 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 updateResellStars: ((CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)?
fileprivate let togglePinnedToTop: ((Bool) -> Bool)?
fileprivate let updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)?
fileprivate let togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)?
fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)?
fileprivate let openChatTheme: (() -> Void)?
public var disposed: () -> Void = {}
@ -4391,13 +4571,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
index: Int? = nil,
forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: (() -> Void)? = nil,
convertToStars: ((StarGiftReference) -> Void)? = 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,
updateResellStars: ((CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil,
openChatTheme: (() -> Void)? = nil
) {
self.context = context
self.subject = subject
@ -4410,6 +4591,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.updateResellStars = updateResellStars
self.togglePinnedToTop = togglePinnedToTop
self.shareStory = shareStory
self.openChatTheme = openChatTheme
if case let .unique(gift) = subject.arguments?.gift, gift.resellForTonOnly {
self.balanceCurrency = .ton
@ -4453,8 +4635,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
if "".isEmpty {
let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable])
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], limit: 50)
self.upgradableDisposable = (upgradableGiftsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
@ -4529,7 +4711,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
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 {
} else {
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 {
presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0)
} else if let value = member.presence {
@ -136,6 +136,17 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
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(
presentationData: ItemListPresentationData(presentationData),
style: .plain,
@ -145,7 +156,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
context: context,
peerMode: .memberList,
peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)),
status: .presence(presence, presentationData.dateTimeFormat),
status: status,
rightLabelText: label,
enabled: true,
selection: .none,

View File

@ -589,14 +589,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
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 {
currentSavedMusic = savedMusicState.files.first
} else if let cachedUserData = screenData.cachedData as? CachedUserData {
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 isLandscape = containerInset > 16.0
@ -2640,7 +2640,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
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 {
transition.updateAlpha(layer: musicBackground.layer, alpha: 1.0 - transitionFraction)
@ -2698,7 +2698,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
environment: {},
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 musicView.superview == nil {
self.regularContentNode.view.addSubview(musicView)

View File

@ -1163,9 +1163,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
return
}
//TODO:localize
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)
}, action: { [weak self] _, f in
guard let self else {
@ -1183,7 +1182,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
guard let self else {
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)
})
}

View File

@ -5012,8 +5012,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added)
},
convertToStars: { [weak profileGifts] in
guard let profileGifts, let reference = gift.reference else {
convertToStars: { [weak profileGifts] reference in
guard let profileGifts else {
return
}
profileGifts.convertStarGift(reference: reference)
@ -5024,8 +5024,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
return profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
},
upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in
guard let profileGifts, let reference = gift.reference else {
upgradeGift: { [weak profileGifts] formId, reference, keepOriginalInfo in
guard let profileGifts else {
return .never()
}
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)
},
convertToStars: { [weak self] in
guard let self, let reference = product.reference else {
convertToStars: { [weak self] reference in
guard let self else {
return
}
self.profileGifts.convertStarGift(reference: reference)
@ -623,8 +623,8 @@ final class GiftsListView: UIView {
}
return self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
},
upgradeGift: { [weak self] formId, keepOriginalInfo in
guard let self, let reference = product.reference else {
upgradeGift: { [weak self] formId, reference, keepOriginalInfo in
guard let self else {
return .never()
}
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)
},
updateResellStars: { [weak self] price in
guard let self, let reference = product.reference else {
updateResellStars: { [weak self] reference, price in
guard let self else {
return .never()
}
return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
},
togglePinnedToTop: { [weak self] pinnedToTop in
togglePinnedToTop: { [weak self] reference, pinnedToTop in
guard let self else {
return false
}
if let reference = product.reference {
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
self.displayUnpinScreen?(product, {
dismissImpl?()
})
return false
}
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
var title = ""
if case let .unique(uniqueGift) = product.gift {
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))"
}
if pinnedToTop {
Queue.mainQueue().after(0.35) {
let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
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))
}
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
self.displayUnpinScreen?(product, {
dismissImpl?()
})
return false
}
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
var title = ""
if case let .unique(uniqueGift) = product.gift {
title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))"
}
if pinnedToTop {
Queue.mainQueue().after(0.35) {
let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string
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))
}
}
return true
@ -996,7 +994,7 @@ final class GiftsListView: UIView {
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>
if let current = self.footerText {
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))
}
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

View File

@ -90,7 +90,6 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private let tabSelector = ComponentView<Empty>()
public private(set) var currentCollection: GiftCollection = .all
private var footerText: ComponentView<Empty>?
private var panelBackground: NavigationBackgroundNode?
private var panelSeparator: ASDisplayNode?
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
}
//TODO:localize
var titleItems: [AnimatedTextComponent.Item] = []
let ratingTitle = environment.strings.ProfileLevelInfo_RatingTitle
let futureTitle = environment.strings.ProfileLevelInfo_FutureRatingTitle
if self.isPreviewingPendingRating {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(0),
isUnbreakable: false,
content: .text("Future ")
))
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1),
isUnbreakable: true,
content: .text("Rating")
))
if let range = futureTitle.range(of: ratingTitle) {
if !futureTitle[..<range.lowerBound].isEmpty {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(0),
isUnbreakable: false,
content: .text(String(futureTitle[..<range.lowerBound]))
))
}
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 {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1),
isUnbreakable: true,
content: .text("Rating")
content: .text(ratingTitle)
))
}

View File

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

View File

@ -273,9 +273,9 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
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)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value = value {
return .single(value)
|> mapToSignal { generatorAndRects -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let (generator, _) = generatorAndRects {
return .single(generator)
} else {
return .complete()
}

View File

@ -673,14 +673,13 @@ final class StarsTransactionsScreenComponent: Component {
let withdrawAvailable = (self.revenueState?.balances.overallRevenue.amount.value ?? 0) > 0
if component.starsContext.ton {
//TODO:localize
let proceedsSize = self.proceedsView.update(
transition: .immediate,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Proceeds Overview".uppercased(),
string: environment.strings.Ton_ProceedsOverview.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
@ -691,14 +690,14 @@ final class StarsTransactionsScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat,
title: "Balance Available to Withdraw",
title: environment.strings.Ton_AvailableBalance,
value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat,
title: "Total Lifetime Proceeds",
title: environment.strings.Ton_LifetimeProceeds,
value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0
)))
@ -725,7 +724,7 @@ final class StarsTransactionsScreenComponent: Component {
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))
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)
@ -754,7 +753,7 @@ final class StarsTransactionsScreenComponent: Component {
},
tapAction: { [weak self] attributes, _ in
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,
@ -766,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component {
count: self.starsState?.balance ?? StarsAmount.zero,
currency: component.starsContext.ton ? .ton : .stars,
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),
actionIsEnabled: true,
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

@ -100,10 +100,23 @@ func openWebAppImpl(
}
}
}
var botPeer = botPeer
if case let .inline(bot) = source {
botPeer = bot
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id))
|> deliverOnMainQueue).start(next: { appSettings in
let openWebView = { [weak parentController] in
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 {
return
}
@ -305,6 +318,11 @@ func openWebAppImpl(
presentImpl = { [weak controller] c, a in
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
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: {
@ -314,25 +332,37 @@ func openWebAppImpl(
}
}
if skipTermsOfService {
openWebView()
} else {
var botPeer = botPeer
if case let .inline(bot) = source {
botPeer = bot
var isAttachMenuBotInstalled: Bool?
if let _ = attachMenuBot {
if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) {
isAttachMenuBotInstalled = true
} else {
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 !noticed || attachMenuBot?.flags.contains(.notActivated) == true || isAttachMenuBotInstalled == false {
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)
}
if value {
openWebView()
} else {
if skipTermsOfService {
openWebView(false)
} else {
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
openWebView()
openWebView(false)
}, showMore: nil, openTerms: {
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: {})
@ -340,7 +370,9 @@ func openWebAppImpl(
})
parentController.present(controller, in: .window(.root))
}
})
}
} else {
openWebView(false)
}
})
}

View File

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

View File

@ -141,6 +141,7 @@ import SuggestedPostApproveAlert
import AVFoundation
import BalanceNeededScreen
import FaceScanScreen
import ChatThemeScreen
public final class ChatControllerOverlayPresentationData {
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))
}
return true
case .setChatTheme:
self.presentThemeSelection()
case let .setChatTheme(chatTheme):
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
case let .setChatWallpaper(wallpaper, _):
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
@ -5808,10 +5830,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
}
case let .gift(gift, wallpaper):
let _ = gift
let _ = wallpaper
//TODO:release
case .gift:
if let darkAppearancePreview = darkAppearancePreview {
useDarkAppearance = darkAppearancePreview
}
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 {
useDarkAppearance = darkAppearancePreview

View File

@ -46,6 +46,7 @@ import ComponentFlow
import ChatEmptyNode
import SpaceWarpView
import ChatSideTopicsPanel
import ChatThemeScreen
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -3582,7 +3583,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
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)
if self.pendingSwitchToChatLocation == nil {

View File

@ -2053,7 +2053,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
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
}
@ -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)
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 {
mappedTransition.options.remove(.AnimateInsertion)

View File

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

View File

@ -57,6 +57,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
private var savedIdsPromise = Promise<Set<Int64>?>()
private var savedIds: Set<Int64>?
private var copyProtectionEnabled = false
init(
context: AccountContext,
chatLocation: ChatLocation,
@ -275,13 +277,17 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
self.historyNode.endedInteractiveDragging = { [weak self] _ in
guard let strongSelf = self else {
guard let self else {
return
}
switch strongSelf.historyNode.visibleContentOffset() {
switch self.historyNode.visibleContentOffset() {
case let .known(value):
if value <= -10.0 {
strongSelf.requestDismiss()
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
} else {
if value <= -10.0 {
self.requestDismiss()
}
}
default:
break
@ -388,14 +394,25 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
})
self.savedIdsDisposable = (context.engine.peers.savedMusicIds()
|> deliverOnMainQueue).start(next: { [weak self] savedIds in
let copyProtectionEnabled: Signal<Bool, NoError>
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 {
return
}
let isFirstTime = self.savedIds == nil
self.savedIds = savedIds
self.savedIdsPromise.set(.single(savedIds))
self.copyProtectionEnabled = copyProtectionEnabled
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring)
self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition)
@ -439,11 +456,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
panRecognizer.shouldBegin = { [weak self] point in
guard let strongSelf = self else {
guard let self else {
return false
}
if strongSelf.controlsNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.controlsNode.view)) {
if strongSelf.controlsNode.frame.maxY <= strongSelf.historyNode.frame.minY {
if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) {
if self.controlsNode.frame.maxY <= self.historyNode.frame.minY {
return true
}
}
@ -494,8 +511,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
return .single(true)
}
})
self.historyNode.useMainQueueTransactions = 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) {
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 {
actionText = nil
}
//TODO:localize
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: "Audio added to your profile.",
text: self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile,
customUndoText: actionText,
timeout: 3.0
),
@ -607,14 +628,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
func removeFromSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips()
//TODO:localize
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: "Audio removed from your profile.",
text: self.presentationData.strings.MediaPlayer_SavedMusic_RemovedFromProfile,
customUndoText: nil,
timeout: 3.0
),
@ -638,6 +658,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
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 {
return nil
}
@ -672,7 +698,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
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
@ -732,7 +757,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
self.requestDismiss()
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = gestureRecognizer as? UIPanGestureRecognizer {
let location = recognizer.location(in: self.view)
@ -981,9 +1006,16 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
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)
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.recursivelyEnsureDisplaySynchronously(true)
@ -1010,10 +1042,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
var items: [ContextMenuItem] = []
//TODO:localize
if canSaveToProfile || canSaveToSavedMessages {
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 {
var subActions: [ContextMenuItem] = []
subActions.append(
@ -1025,7 +1056,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToProfile {
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)
if let self {
@ -1037,7 +1068,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToSavedMessages {
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)
if let self {
@ -1048,7 +1079,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
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)
if let self {
@ -1072,7 +1103,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil
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))))
@ -1080,7 +1111,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}))
)
} 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)
}, action: { [weak self] _, f in
f(.default)
@ -1112,7 +1143,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
addedSeparator = true
}
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)
guard let self else {
@ -1125,7 +1156,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
// 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)
//
// if let _ = self {
@ -1162,9 +1193,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
items.append(.separator)
addedSeparator = true
}
var actionTitle = "Delete"
var actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Delete
if case .custom = self.source {
actionTitle = "Remove"
actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Remove
}
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

View File

@ -1026,13 +1026,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.separatorNode.isHidden = hasSectionHeader
if hasSectionHeader {
//TODO:localize
let sideInset: CGFloat = 16.0
var sectionTitle = "AUDIO IN THIS CHAT"
var sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_ThisChat
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 {
sectionTitle = "YOUR PLAYLIST"
sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusicYou
}
let sectionTitleSize = self.sectionTitle.update(
transition: .immediate,
@ -1096,7 +1095,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
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 {
attributedString.addAttribute(.attachment, value: chevronImage, 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
} else {
//TODO:localize
profileAudioComponent = AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
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)
)),
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)
)),

View File

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

View File

@ -8,6 +8,7 @@ import TelegramCore
import AccountContext
import SwiftSignalKit
import WallpaperResources
import StickerResources
import FastBlur
import Svg
import GZip
@ -85,6 +86,7 @@ public protocol WallpaperBackgroundNode: ASDisplayNode {
var rotation: CGFloat { get set }
func update(wallpaper: TelegramWallpaper, animated: Bool)
func update(wallpaper: TelegramWallpaper, starGift: StarGift?, animated: Bool)
func _internalUpdateIsSettingUpWallpaper()
func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition)
func updateIsLooping(_ isLooping: Bool)
@ -758,11 +760,20 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private var validLayout: (CGSize, WallpaperDisplayMode)?
private var wallpaper: TelegramWallpaper?
private var starGift: StarGift?
private var modelRectIndex: Int32?
private var modelStickerNode: DefaultAnimatedStickerNodeImpl?
private var isSettingUpWallpaper: Bool = false
private struct CachedValidPatternImage {
let generate: (TransformImageArguments) -> DrawingContext?
let generated: ValidPatternGeneratedImage
let rects: [WallpaperGiftPatternRect]
let starGift: StarGift?
let symbolImage: UIImage?
let modelRectIndex: Int32?
let image: UIImage
}
@ -771,6 +782,10 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
private struct ValidPatternImage {
let wallpaper: TelegramWallpaper
let invertPattern: Bool
let rects: [WallpaperGiftPatternRect]
let starGift: StarGift?
let symbolImage: UIImage?
let modelRectIndex: Int32?
let generate: (TransformImageArguments) -> DrawingContext?
}
private var validPatternImage: ValidPatternImage?
@ -781,10 +796,38 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
let patternColor: UInt32
let backgroundColor: UInt32
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 let patternImageDisposable = MetaDisposable()
private let symbolImageDisposable = MetaDisposable()
private var bubbleTheme: PresentationTheme?
private var bubbleCorners: PresentationChatBubbleCorners?
@ -930,11 +973,26 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
}
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
}
let previousWallpaper = self.wallpaper
let previousStarGift = self.starGift
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 snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
@ -1132,6 +1190,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
}
default:
self.patternImageDisposable.set(nil)
self.symbolImageDisposable.set(nil)
self.validPatternImage = nil
self.patternImageLayer.isHidden = true
self.patternImageLayer.fillWithColorUntilLoaded = nil
@ -1146,6 +1205,9 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
guard let wallpaper = self.wallpaper else {
return
}
let starGift = self.starGift
let modelRectIndex = self.modelRectIndex
var invertPattern: Bool = false
var patternIsLight: Bool = false
@ -1169,13 +1231,20 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
break
}
}
if let previousStarGift = self.validPatternImage?.starGift, !updated {
updated = true
if previousStarGift.slug == starGift?.slug {
updated = false
}
}
if updated {
self.validPatternGeneratedImage = nil
self.validPatternImage = nil
if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper && cachedValidPatternImage.generated.invertPattern == invertPattern {
self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, invertPattern: invertPattern, generate: cachedValidPatternImage.generate)
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, rects: cachedValidPatternImage.rects, starGift: cachedValidPatternImage.starGift, symbolImage: cachedValidPatternImage.symbolImage, modelRectIndex: cachedValidPatternImage.modelRectIndex, generate: cachedValidPatternImage.generate)
} else {
func reference(for resource: EngineMediaResource, media: EngineMedia) -> MediaResourceReference {
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))))
let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true)
self.patternImageDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] generator in
guard let strongSelf = self else {
var symbolImage: Signal<UIImage?, NoError> = .single(nil)
if let starGift = self.starGift, case let .unique(uniqueGift) = starGift {
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
}
if let generator = generator {
/*generator = { arguments in
let scale = arguments.scale ?? UIScreenScale
let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true)
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)
if let (generator, rects) = generator {
self.validPatternImage = ValidPatternImage(wallpaper: wallpaper, invertPattern: invertPattern, rects: rects, starGift: starGift, symbolImage: symbolImage, modelRectIndex: modelRectIndex, generate: generator)
self.validPatternGeneratedImage = nil
if let (size, displayMode) = self.validLayout {
self.loadPatternForSizeIfNeeded(size: size, displayMode: displayMode, transition: .immediate)
} else {
strongSelf._isReady.set(true)
self._isReady.set(true)
}
} else {
strongSelf._isReady.set(true)
self._isReady.set(true)
}
}))
}
@ -1244,8 +1309,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
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 {
self.validPatternGeneratedImage = updatedGeneratedImage
@ -1256,7 +1321,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.patternImageLayer.suspendCompositionUpdates = false
self.patternImageLayer.updateCompositionIfNeeded()
} 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 let drawingContext = validPatternImage.generate(patternArguments) {
if let image = drawingContext.generateImage() {
@ -1267,7 +1332,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.patternImageLayer.updateCompositionIfNeeded()
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 {
self.updatePatternPresentation()
@ -1288,7 +1353,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
strongSelf.updatePatternPresentation()
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)
}
}
}
@ -1306,6 +1371,68 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
self.updatePatternPresentation()
}
}
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))
}
@ -1559,3 +1686,14 @@ private protocol WallpaperComponentView: AnyObject {
public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false) -> WallpaperBackgroundNode {
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 bakePatternAlpha: CGFloat
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.rotation = rotation
self.customPatternColor = customPatternColor
self.preview = preview
self.bakePatternAlpha = bakePatternAlpha
self.displayMode = displayMode
self.symbolImage = symbolImage
self.modelRectIndex = modelRectIndex
}
public func serialized() -> NSArray {
@ -373,6 +377,9 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments {
array.add(NSNumber(value: self.preview))
array.add(NSNumber(value: Double(self.bakePatternAlpha)))
array.add(NSNumber(value: self.displayMode.rawValue))
if let symbolImage {
array.add(symbolImage)
}
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)
|> mapToSignal { fullSizeData, fullSizeComplete in
if !autoFetchFullSize || fullSizeComplete {
return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode)
return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode, forcePrepared: forcePrepared)
} else {
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
if case .thumbnail = mode {
prominent = true
@ -491,7 +514,11 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
return .single((fullSizeData, fullSizeComplete))
|> 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
if scale.isZero {
scale = arguments.scale ?? UIScreenScale
@ -561,12 +588,12 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
var image: UIImage?
if let fullSizeData = fullSizeData {
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 {
image = UIImage(data: fullSizeData)
}
}
if let customPatternColor = customArguments.customPatternColor, customPatternColor.alpha < 1.0 {
patternIsInverted = true
c.setBlendMode(.copy)
@ -674,7 +701,7 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete
} else {
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))
}
case let .file(file):
if file.settings.intensity == 100 {
print()
}
rotation = file.settings.rotation
if file.isPattern, let intensity = file.settings.intensity, intensity < 0 {
backgroundColor = (.black, nil, [])
@ -1464,6 +1495,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
} else {
backgroundColor = (theme.chatList.backgroundColor, nil, [])
}
wallpaperSignal = cachedWallpaper(account: account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper in
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 {
ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource))
}
let useFallback = convertedPreviewRepresentations.isEmpty
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)))
@ -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
arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white)
}
return patternWallpaperImage(account: account, accountManager: accountManager, representations: convertedPreviewRepresentations, mode: .thumbnail, autoFetchFullSize: true)
|> mapToSignal { generator -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in
return patternWallpaperImage(account: account, accountManager: accountManager, representations: useFallback ? convertedRepresentations : convertedPreviewRepresentations, mode: useFallback ? .screen : .thumbnail, autoFetchFullSize: true)
|> mapToSignal { generatorAndRects -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in
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 context = generator?(imageArguments)
let context = generatorAndRects?.generator(imageArguments)
let image = context?.generateImage()
if !file.settings.colors.isEmpty {
@ -1771,7 +1803,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
c.restoreGState()
} 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.clip()

View File

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